Skip to content

Commit 8792639

Browse files
committed
chore: add callout to enable offline access. only send scopes if specifed at least 1.
1 parent 3965c38 commit 8792639

File tree

4 files changed

+125
-7
lines changed

4 files changed

+125
-7
lines changed

EXAMPLES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2302,6 +2302,9 @@ The connect endpoint (`/auth/connect` or your custom path) accepts the following
23022302
- `scopes`: (optional) defines the permissions that the client requests from the Identity Provider.. Can be specified as multiple values (e.g., `?scopes=openid&scopes=profile&scopes=email`) or using bracket notation (e.g., `?scopes[]=openid&scopes[]=profile&scopes[]=email`).
23032303
- Any additional parameters will be passed as the `authorizationParams` in the call to `/me/v1/connected-accounts/connect`.
23042304
2305+
> [!IMPORTANT]
2306+
> You must enable `Offline Access` from the Connection Permissions settings to be able to use the connection with Connected Accounts.
2307+
23052308
### `onCallback` hook
23062309
23072310
When a user is redirected back to your application after completing the connected accounts flow, the `onCallback` hook will be called. You can use this hook to run custom logic after the user has connected their account, like so:
@@ -2347,6 +2350,9 @@ export async function GET() {
23472350
}
23482351
```
23492352
2353+
> [!IMPORTANT]
2354+
> You must enable `Offline Access` from the Connection Permissions settings to be able to use the connection with Connected Accounts.
2355+
23502356
## Back-Channel Logout
23512357
23522358
The SDK can be configured to listen to [Back-Channel Logout](https://auth0.com/docs/authenticate/login/logout/back-channel-logout) events. By default, a route will be mounted `/auth/backchannel-logout` which will verify the logout token and call the `deleteByLogoutToken` method of your session store implementation to allow you to remove the session.

src/server/auth-client.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6549,6 +6549,112 @@ ca/T0LLtgmbMmxSv/MmzIg==
65496549
const response = await authClient.handler(request);
65506550
expect(response.status).toEqual(400);
65516551
});
6552+
6553+
it("should only forward the scopes if at least one scope is requested", async () => {
6554+
const currentAccessToken = DEFAULT.accessToken;
6555+
const newAccessToken = "at_456";
6556+
const secret = await generateSecret(32);
6557+
let connectAccountRequestBody: any;
6558+
const transactionStore = new TransactionStore({
6559+
secret
6560+
});
6561+
const sessionStore = new StatelessSessionStore({
6562+
secret
6563+
});
6564+
const authClient = new AuthClient({
6565+
transactionStore,
6566+
sessionStore,
6567+
6568+
domain: DEFAULT.domain,
6569+
clientId: DEFAULT.clientId,
6570+
clientSecret: DEFAULT.clientSecret,
6571+
6572+
secret,
6573+
appBaseUrl: DEFAULT.appBaseUrl,
6574+
6575+
routes: getDefaultRoutes(),
6576+
6577+
fetch: getMockAuthorizationServer({
6578+
tokenEndpointResponse: {
6579+
token_type: "Bearer",
6580+
access_token: newAccessToken,
6581+
scope: "openid profile email offline_access",
6582+
expires_in: 86400 // expires in 10 days
6583+
} as oauth.TokenEndpointResponse,
6584+
onConnectAccountRequest: async (req) => {
6585+
connectAccountRequestBody = await req.json();
6586+
expect(connectAccountRequestBody.scopes).toBeUndefined();
6587+
}
6588+
}),
6589+
6590+
enableConnectAccountEndpoint: true
6591+
});
6592+
6593+
const expiresAt = Math.floor(Date.now() / 1000) + 10 * 24 * 60 * 60; // expires in 10 days
6594+
const session: SessionData = {
6595+
user: {
6596+
sub: DEFAULT.sub,
6597+
name: "John Doe",
6598+
email: "john@example.com",
6599+
picture: "https://example.com/john.jpg"
6600+
},
6601+
tokenSet: {
6602+
accessToken: currentAccessToken,
6603+
scope: "openid profile email",
6604+
refreshToken: DEFAULT.refreshToken,
6605+
expiresAt
6606+
},
6607+
internal: {
6608+
sid: DEFAULT.sid,
6609+
createdAt: Math.floor(Date.now() / 1000)
6610+
}
6611+
};
6612+
const maxAge = 60 * 60; // 1 hour
6613+
const expiration = Math.floor(Date.now() / 1000 + maxAge);
6614+
const sessionCookie = await encrypt(session, secret, expiration);
6615+
const headers = new Headers();
6616+
headers.append("cookie", `__session=${sessionCookie}`);
6617+
const url = new URL("/auth/connect", DEFAULT.appBaseUrl);
6618+
url.searchParams.append("connection", DEFAULT.connectAccount.connection);
6619+
url.searchParams.append("returnTo", "/some-url");
6620+
url.searchParams.append("audience", "urn:some-audience");
6621+
6622+
const request = new NextRequest(url, {
6623+
method: "GET",
6624+
headers
6625+
});
6626+
6627+
const response = await authClient.handler(request);
6628+
expect(response.status).toEqual(307);
6629+
const connectUrl = new URL(response.headers.get("location")!);
6630+
expect(connectUrl.origin).toEqual(`https://${DEFAULT.domain}`);
6631+
expect(connectUrl.pathname).toEqual("/connect");
6632+
expect(connectUrl.searchParams.get("ticket")).toEqual(
6633+
DEFAULT.connectAccount.ticket
6634+
);
6635+
6636+
// transaction state
6637+
const transactionCookie = response.cookies.get(
6638+
`__txn_${connectAccountRequestBody.state}`
6639+
);
6640+
expect(transactionCookie).toBeDefined();
6641+
expect(
6642+
(
6643+
(await decrypt(
6644+
transactionCookie!.value,
6645+
secret
6646+
)) as jose.JWTDecryptResult
6647+
).payload
6648+
).toEqual(
6649+
expect.objectContaining({
6650+
responseType: RESPONSE_TYPES.CONNECT_CODE,
6651+
state: connectAccountRequestBody?.state,
6652+
returnTo: "/some-url",
6653+
codeVerifier: expect.any(String),
6654+
authSession: DEFAULT.connectAccount.authSession
6655+
})
6656+
);
6657+
});
65526658
});
65536659

65546660
describe("getTokenSet", async () => {

src/server/auth-client.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,14 +1054,18 @@ export class AuthClient {
10541054
}
10551055

10561056
const { tokenSet } = getTokenSetResponse;
1057+
const connectAccountParams: ConnectAccountOptions = {
1058+
connection,
1059+
authorizationParams,
1060+
returnTo
1061+
};
1062+
1063+
if (scopes.length > 0) {
1064+
connectAccountParams.scopes = scopes;
1065+
}
1066+
10571067
const [connectAccountError, connectAccountResponse] =
1058-
await this.connectAccount({
1059-
tokenSet: tokenSet,
1060-
connection,
1061-
scopes,
1062-
authorizationParams,
1063-
returnTo
1064-
});
1068+
await this.connectAccount({ tokenSet, ...connectAccountParams });
10651069

10661070
if (connectAccountError) {
10671071
return new NextResponse(connectAccountError.message, {

src/server/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,8 @@ export class Auth0Client {
10071007
* for the My Account API to create a connected account for the user.
10081008
*
10091009
* The user will then be redirected to authorize the connection with the third-party provider.
1010+
*
1011+
* You must enable `Offline Access` from the Connection Permissions settings to be able to use the connection with Connected Accounts.
10101012
*/
10111013
async connectAccount(options: ConnectAccountOptions): Promise<NextResponse> {
10121014
const session = await this.getSession();

0 commit comments

Comments
 (0)