Skip to content

Commit

Permalink
4.0.0-beta.8 (#1811)
Browse files Browse the repository at this point in the history
  • Loading branch information
guabu authored Nov 25, 2024
1 parent 06888ed commit 7a8e677
Show file tree
Hide file tree
Showing 16 changed files with 238 additions and 127 deletions.
50 changes: 39 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
### 1. Install the SDK

```shell
npm i @auth0/[email protected].7
npm i @auth0/[email protected].8
```

### 2. Add the environment variables
Expand Down Expand Up @@ -34,7 +34,7 @@ The `APP_BASE_URL` is the URL that your application is running on. When developi
> You will need to register the follwing URLs in your Auth0 Application via the [Auth0 Dashboard](https://manage.auth0.com):
>
> - Add `http://localhost:3000/auth/callback` to the list of **Allowed Callback URLs**
> - Add `http://localhost:3000/auth/logout` to the list of **Allowed Logout URLs**
> - Add `http://localhost:3000` to the list of **Allowed Logout URLs**
### 3. Create the Auth0 SDK client

Expand Down Expand Up @@ -259,9 +259,12 @@ import { getAccessToken } from "@auth0/nextjs-auth0"

export default function Component() {
async function fetchData() {
const token = await getAccessToken()

// call external API with the token...
try {
const token = await auth0.getAccessToken()
// call external API with token...
} catch (err) {
// err will be an instance of AccessTokenError if an access token could not be obtained
}
}

return (
Expand All @@ -282,9 +285,12 @@ import { NextResponse } from "next/server"
import { auth0 } from "@/lib/auth0"

export async function GET() {
const token = await auth0.getAccessToken()

// call external API with token...
try {
const token = await auth0.getAccessToken()
// call external API with token...
} catch (err) {
// err will be an instance of AccessTokenError if an access token could not be obtained
}

return NextResponse.json({
message: "Success!",
Expand All @@ -305,9 +311,12 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse<{ message: string }>
) {
const token = await auth0.getAccessToken(req)

// call external API with token...
try {
const token = await auth0.getAccessToken(req)
// call external API with token...
} catch (err) {
// err will be an instance of AccessTokenError if an access token could not be obtained
}

res.status(200).json({ message: "Success!" })
}
Expand Down Expand Up @@ -451,6 +460,25 @@ export async function middleware(request: NextRequest) {

For a complete example using `next-intl` middleware, please see the `examples/` directory of this repository.

## ID Token claims and the user object

By default, the following properties claims from the ID token are added to the `user` object in the session automatically:

- `sub`
- `name`
- `nickname`
- `given_name`
- `family_name`
- `picture`
- `email`
- `email_verified`
- `org_id`

If you'd like to customize the `user` object to include additional custom claims from the ID token, you can use the `beforeSessionSaved` hook (see [beforeSessionSaved hook](#beforesessionsaved))

> [!NOTE]
> It's best practice to limit what claims are stored on the `user` object in the session to avoid bloating the session cookie size and going over browser limits.
## Routes

The SDK mounts 6 routes:
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@auth0/nextjs-auth0",
"version": "4.0.0-beta.7",
"version": "4.0.0-beta.8",
"description": "Auth0 Next.js SDK",
"main": "dist/index.js",
"scripts": {
Expand Down Expand Up @@ -52,6 +52,12 @@
},
"./server": {
"import": "./dist/server/index.js"
},
"./errors": {
"import": "./dist/errors/index.js"
},
"./types": {
"import": "./dist/types/index.d.ts"
}
},
"dependencies": {
Expand Down
26 changes: 23 additions & 3 deletions src/client/helpers/get-access-token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { AccessTokenError } from "../../errors"

export async function getAccessToken() {
// TODO: cache response and invalidate according to expiresAt
const tokenRes = await fetch("/auth/access-token").then((res) => res.json())
const tokenRes = await fetch("/auth/access-token")

if (!tokenRes.ok) {
// try to parse it as JSON and throw the error from the API
// otherwise, throw a generic error
let accessTokenError
try {
accessTokenError = await tokenRes.json()
} catch (e) {
throw new Error(
"An unexpected error occurred while trying to fetch the access token."
)
}

throw new AccessTokenError(
accessTokenError.error.code,
accessTokenError.error.message
)
}

return tokenRes.token
const tokenSet = await tokenRes.json()
return tokenSet.token
}
21 changes: 18 additions & 3 deletions src/client/hooks/use-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@

import useSWR from "swr"

import { User } from "../../server/user"
import type { User } from "../../types"

export function useUser() {
const { data, error, isLoading } = useSWR<User, {}, string>(
const { data, error, isLoading } = useSWR<User, Error, string>(
"/auth/profile",
(...args) => fetch(...args).then((res) => res.json())
(...args) =>
fetch(...args).then((res) => {
if (!res.ok) {
throw new Error("Unauthorized")
}

return res.json()
})
)

if (error) {
return {
user: null,
isLoading: false,
error,
}
}

return {
user: data,
isLoading,
Expand Down
46 changes: 20 additions & 26 deletions src/errors.ts → src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ export abstract class SdkError extends Error {
public abstract code: string
}

/**
* Errors that come from Auth0 in the `redirect_uri` callback may contain reflected user input via the OpenID Connect `error` and `error_description` query parameter.
* You should **not** render the error `message`, or `error` and `error_description` properties without properly escaping them first.
*/
export class OAuth2Error extends SdkError {
public code: string

constructor({ code, message }: { code: string; message?: string }) {
// TODO: sanitize error message or add warning
super(
message ??
"An error occured while interacting with the authorization server."
Expand All @@ -25,31 +28,6 @@ export class DiscoveryError extends SdkError {
}
}

export class MissingRefreshToken extends SdkError {
public code: string = "missing_refresh_token"

constructor(message?: string) {
super(
message ??
"The access token has expired and a refresh token was not granted."
)
this.name = "MissingRefreshToken"
}
}

export class RefreshTokenGrantError extends SdkError {
public code: string = "refresh_token_grant_error"
public cause: OAuth2Error

constructor({ cause, message }: { cause: OAuth2Error; message?: string }) {
super(
message ?? "An error occured while trying to refresh the access token."
)
this.cause = cause
this.name = "RefreshTokenGrantError"
}
}

export class MissingStateError extends SdkError {
public code: string = "missing_state"

Expand Down Expand Up @@ -104,3 +82,19 @@ export class BackchannelLogoutError extends SdkError {
this.name = "BackchannelLogoutError"
}
}

export enum AccessTokenErrorCode {
MISSING_SESSION = "missing_session",
MISSING_REFRESH_TOKEN = "missing_refresh_token",
FAILED_TO_REFRESH_TOKEN = "failed_to_refresh_token",
}

export class AccessTokenError extends SdkError {
public code: string

constructor(code: string, message: string) {
super(message)
this.name = "AccessTokenError"
this.code = code
}
}
30 changes: 19 additions & 11 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import * as oauth from "oauth4webapi"
import { describe, expect, it, vi } from "vitest"

import { generateSecret } from "../test/utils"
import { SessionData } from "../types"
import { AuthClient } from "./auth-client"
import { decrypt, encrypt } from "./cookies"
import { SessionData } from "./session/abstract-session-store"
import { StatefulSessionStore } from "./session/stateful-session-store"
import { StatelessSessionStore } from "./session/stateless-session-store"
import { TransactionState, TransactionStore } from "./transaction-store"
Expand Down Expand Up @@ -1287,7 +1287,7 @@ describe("Authentication Client", async () => {
expect(cookie?.expires).toEqual(new Date("1970-01-01T00:00:00.000Z"))
})

it("should return an error if the client does not have RP-Initiated Logout enabled", async () => {
it("should fallback to the /v2/logout endpoint if the client does not have RP-Initiated Logout enabled", async () => {
const secret = await generateSecret(32)
const transactionStore = new TransactionStore({
secret,
Expand Down Expand Up @@ -1330,10 +1330,13 @@ describe("Authentication Client", async () => {
)

const response = await authClient.handleLogout(request)
expect(response.status).toEqual(500)
expect(await response.text()).toEqual(
"An error occured while trying to initiate the logout request."
)
expect(response.status).toEqual(307)
const logoutUrl = new URL(response.headers.get("Location")!)
expect(logoutUrl.origin).toEqual(`https://${DEFAULT.domain}`)

// query parameters
expect(logoutUrl.searchParams.get("client_id")).toEqual(DEFAULT.clientId)
expect(logoutUrl.searchParams.get("returnTo")).toEqual(DEFAULT.appBaseUrl)
})

it("should return an error if the discovery endpoint could not be fetched", async () => {
Expand Down Expand Up @@ -2523,7 +2526,10 @@ describe("Authentication Client", async () => {
const response = await authClient.handleAccessToken(request)
expect(response.status).toEqual(401)
expect(await response.json()).toEqual({
error: "You are not authenticated.",
error: {
message: "The user does not have an active session.",
code: "missing_session",
},
})

// validate that the session cookie has not been set
Expand Down Expand Up @@ -2585,9 +2591,11 @@ describe("Authentication Client", async () => {
const response = await authClient.handleAccessToken(request)
expect(response.status).toEqual(401)
expect(await response.json()).toEqual({
error_code: "missing_refresh_token",
error:
"The access token has expired and a refresh token was not granted.",
error: {
message:
"The access token has expired and a refresh token was not provided. The user needs to re-authenticate.",
code: "missing_refresh_token",
},
})

// validate that the session cookie has not been set
Expand Down Expand Up @@ -3272,7 +3280,7 @@ describe("Authentication Client", async () => {
}

const [error, updatedTokenSet] = await authClient.getTokenSet(tokenSet)
expect(error?.code).toEqual("refresh_token_grant_error")
expect(error?.code).toEqual("failed_to_refresh_token")
expect(updatedTokenSet).toBeNull()
})

Expand Down
Loading

0 comments on commit 7a8e677

Please sign in to comment.