Skip to content

Commit

Permalink
4.0.0-beta.13 (#1850)
Browse files Browse the repository at this point in the history
  • Loading branch information
guabu authored Dec 20, 2024
1 parent 5bd9a1e commit 92df43b
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 122 deletions.
21 changes: 13 additions & 8 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.12
npm i @auth0/nextjs-auth0@beta
```

### 2. Add the environment variables
Expand Down Expand Up @@ -115,8 +115,8 @@ You can customize the client by using the options below:
| clientId | `string` | The Auth0 client ID. If it's not specified, it will be loaded from the `AUTH0_CLIENT_ID` environment variable. |
| clientSecret | `string` | The Auth0 client secret. If it's not specified, it will be loaded from the `AUTH0_CLIENT_SECRET` environment variable. |
| authorizationParameters | `AuthorizationParameters` | The authorization parameters to pass to the `/authorize` endpoint. See [Passing authorization parameters](#passing-authorization-parameters) for more details. |
| clientAssertionSigningKey | `string` or `CryptoKey` | Private key for use with `private_key_jwt` clients. |
| clientAssertionSigningAlg | `string` | The algorithm used to sign the client assertion JWT. |
| clientAssertionSigningKey | `string` or `CryptoKey` | Private key for use with `private_key_jwt` clients. This can also be specified via the `AUTH0_CLIENT_ASSERTION_SIGNING_KEY` environment variable. |
| clientAssertionSigningAlg | `string` | The algorithm used to sign the client assertion JWT. This can also be provided via the `AUTH0_CLIENT_ASSERTION_SIGNING_ALG` environment variable. |
| appBaseUrl | `string` | The URL of your application (e.g.: `http://localhost:3000`). If it's not specified, it will be loaded from the `APP_BASE_URL` environment variable. |
| secret | `string` | A 32-byte, hex-encoded secret used for encrypting cookies. If it's not specified, it will be loaded from the `AUTH0_SECRET` environment variable. |
| signInReturnToPath | `string` | The path to redirect the user to after successfully authenticating. Defaults to `/`. |
Expand Down Expand Up @@ -351,7 +351,12 @@ export default function Component() {

### On the server (App Router)

On the server, the `getAccessToken()` helper can be used in Server Components, Server Routes, Server Actions, and middleware to get an access token to call external APIs, like so:
On the server, the `getAccessToken()` helper can be used in Server Routes, Server Actions, Server Components, and middleware to get an access token to call external APIs.

> [!IMPORTANT]
> Server Components cannot set cookies. Calling `getAccessToken()` in a Server Component will cause the access token to be refreshed, if it is expired, and the updated token set will not to be persisted.
For example:

```tsx
import { NextResponse } from "next/server"
Expand All @@ -374,7 +379,7 @@ export async function GET() {

### On the server (Pages Router)

On the server, the `getAccessToken(req)` helper can be used in `getServerSideProps`, API routes, and middleware to get an access token to call external APIs, like so:
On the server, the `getAccessToken(req, res)` helper can be used in `getServerSideProps`, API routes, and middleware to get an access token to call external APIs, like so:

```tsx
import type { NextApiRequest, NextApiResponse } from "next"
Expand All @@ -386,7 +391,7 @@ export default async function handler(
res: NextApiResponse<{ message: string }>
) {
try {
const token = await auth0.getAccessToken(req)
const token = await auth0.getAccessToken(req, res)
// call external API with token...
} catch (err) {
// err will be an instance of AccessTokenError if an access token could not be obtained
Expand Down Expand Up @@ -434,11 +439,11 @@ The SDK exposes hooks to enable you to provide custom logic that would be run at

The `beforeSessionSaved` hook is run right before the session is persisted. It provides a mechanism to modify the session claims before persisting them.

The hook recieves a `SessionData` object and must return a Promise that resolves to a `SessionData` object: `(session: SessionData) => Promise<SessionData>`. For example:
The hook recieves a `SessionData` object and an ID token. The function must return a Promise that resolves to a `SessionData` object: `(session: SessionData) => Promise<SessionData>`. For example:

```ts
export const auth0 = new Auth0Client({
async beforeSessionSaved(session) {
async beforeSessionSaved(session, idToken) {
return {
...session,
user: {
Expand Down
2 changes: 1 addition & 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.12",
"version": "4.0.0-beta.13",
"description": "Auth0 Next.js SDK",
"scripts": {
"build": "tsc",
Expand Down
157 changes: 80 additions & 77 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,18 +435,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
}
)

const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60 // expired 10 days ago
const updatedTokenSet = {
accessToken: "at_456",
refreshToken: "rt_456",
expiresAt,
}
authClient.getTokenSet = vi
.fn()
.mockResolvedValue([null, updatedTokenSet])

const response = await authClient.handler(request)
expect(authClient.getTokenSet).toHaveBeenCalled()

// assert session has been updated
const updatedSessionCookie = response.cookies.get("__session")
Expand All @@ -460,8 +449,8 @@ ca/T0LLtgmbMmxSv/MmzIg==
sub: DEFAULT.sub,
},
tokenSet: {
accessToken: "at_456",
refreshToken: "rt_456",
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: expect.any(Number),
},
internal: {
Expand Down Expand Up @@ -516,70 +505,6 @@ ca/T0LLtgmbMmxSv/MmzIg==
const updatedSessionCookie = response.cookies.get("__session")
expect(updatedSessionCookie).toBeUndefined()
})

it("should pass the request through if there was an error fetching the updated token set", async () => {
const secret = await generateSecret(32)
const transactionStore = new TransactionStore({
secret,
})
const sessionStore = new StatelessSessionStore({
secret,

rolling: true,
absoluteDuration: 3600,
inactivityDuration: 1800,
})
const authClient = new AuthClient({
transactionStore,
sessionStore,

domain: DEFAULT.domain,
clientId: DEFAULT.clientId,
clientSecret: DEFAULT.clientSecret,

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: getMockAuthorizationServer(),
})

const session: SessionData = {
user: { sub: DEFAULT.sub },
tokenSet: {
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt: 123456,
},
internal: {
sid: DEFAULT.sid,
createdAt: Math.floor(Date.now() / 1000),
},
}
const sessionCookie = await encrypt(session, secret)
const headers = new Headers()
headers.append("cookie", `__session=${sessionCookie}`)
const request = new NextRequest(
"https://example.com/dashboard/projects",
{
method: "GET",
headers,
}
)

authClient.getTokenSet = vi
.fn()
.mockResolvedValue([
new Error("error fetching updated token set"),
null,
])

const response = await authClient.handler(request)
expect(authClient.getTokenSet).toHaveBeenCalled()

// assert session has not been updated
const updatedSessionCookie = response.cookies.get("__session")
expect(updatedSessionCookie).toBeUndefined()
})
})

describe("with custom routes", async () => {
Expand Down Expand Up @@ -2839,6 +2764,84 @@ ca/T0LLtgmbMmxSv/MmzIg==
})

describe("beforeSessionSaved hook", async () => {
it("should be called with the correct arguments", async () => {
const state = "transaction-state"
const code = "auth-code"

const secret = await generateSecret(32)
const transactionStore = new TransactionStore({
secret,
})
const sessionStore = new StatelessSessionStore({
secret,
})
const mockBeforeSessionSaved = vi.fn().mockResolvedValue({
user: {
sub: DEFAULT.sub,
},
internal: {
sid: DEFAULT.sid,
expiresAt: expect.any(Number),
},
})
const authClient = new AuthClient({
transactionStore,
sessionStore,

domain: DEFAULT.domain,
clientId: DEFAULT.clientId,
clientSecret: DEFAULT.clientSecret,

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: getMockAuthorizationServer(),

beforeSessionSaved: mockBeforeSessionSaved,
})

const url = new URL("/auth/callback", DEFAULT.appBaseUrl)
url.searchParams.set("code", code)
url.searchParams.set("state", state)

const headers = new Headers()
const transactionState: TransactionState = {
nonce: "nonce-value",
maxAge: 3600,
codeVerifier: "code-verifier",
responseType: "code",
state: state,
returnTo: "/dashboard",
}
headers.set(
"cookie",
`__txn_${state}=${await encrypt(transactionState, secret)}`
)
const request = new NextRequest(url, {
method: "GET",
headers,
})

await authClient.handleCallback(request)
expect(mockBeforeSessionSaved).toHaveBeenCalledWith(
{
user: expect.objectContaining({
sub: DEFAULT.sub,
}),
tokenSet: {
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt: expect.any(Number),
},
internal: {
sid: expect.any(String),
createdAt: expect.any(Number),
},
},
expect.any(String)
)
})

it("should use the return value of the hook as the session data", async () => {
const state = "transaction-state"
const code = "auth-code"
Expand Down
38 changes: 19 additions & 19 deletions src/server/auth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { TransactionState, TransactionStore } from "./transaction-store"
import { filterClaims } from "./user"

export type BeforeSessionSavedHook = (
session: SessionData
session: SessionData,
idToken: string | null
) => Promise<SessionData>

type OnCallbackContext = {
Expand Down Expand Up @@ -114,7 +115,7 @@ export class AuthClient {
private clientSecret?: string
private clientAssertionSigningKey?: string | CryptoKey
private clientAssertionSigningAlg: string
private issuer: string
private domain: string
private authorizationParameters: AuthorizationParameters
private pushedAuthorizationRequests: boolean

Expand Down Expand Up @@ -147,11 +148,7 @@ export class AuthClient {
this.sessionStore = options.sessionStore

// authorization server
this.issuer =
options.domain.startsWith("http://") ||
options.domain.startsWith("https://")
? options.domain
: `https://${options.domain}`
this.domain = options.domain
this.clientMetadata = { client_id: options.clientId }
this.clientSecret = options.clientSecret
this.authorizationParameters = options.authorizationParameters || {
Expand Down Expand Up @@ -224,20 +221,10 @@ export class AuthClient {
const session = await this.sessionStore.get(req.cookies)

if (session) {
// refresh the access token, if necessary
const [error, updatedTokenSet] = await this.getTokenSet(
session.tokenSet
)

if (error) {
return res
}

// we pass the existing session (containing an `createdAt` timestamp) to the set method
// which will update the cookie's `maxAge` property based on the `createdAt` time
await this.sessionStore.set(req.cookies, res.cookies, {
...session,
tokenSet: updatedTokenSet,
})
}

Expand Down Expand Up @@ -452,8 +439,14 @@ export class AuthClient {
const res = await this.onCallback(null, onCallbackCtx, session)

if (this.beforeSessionSaved) {
const { user } = await this.beforeSessionSaved(session)
session.user = user || {}
const updatedSession = await this.beforeSessionSaved(
session,
oidcRes.id_token ?? null
)
session = {
...updatedSession,
internal: session.internal,
}
} else {
session.user = filterClaims(idTokenClaims)
}
Expand Down Expand Up @@ -878,4 +871,11 @@ export class AuthClient {
? oauth.PrivateKeyJwt(clientPrivateKey)
: oauth.ClientSecretPost(this.clientSecret!)
}

private get issuer(): string {
return this.domain.startsWith("http://") ||
this.domain.startsWith("https://")
? this.domain
: `https://${this.domain}`
}
}
Loading

0 comments on commit 92df43b

Please sign in to comment.