Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.0.0-beta.8 #1811

Merged
merged 10 commits into from
Nov 25, 2024
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/nextjs-auth0@4.0.0-beta.7
npm i @auth0/nextjs-auth0@4.0.0-beta.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