Skip to content

Commit

Permalink
Add support for Federated Connection Access Token (#1911)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Pandey <[email protected]>
  • Loading branch information
frederikprijck and tusharpandey13 authored Feb 13, 2025
1 parent 6487987 commit 7a253a2
Show file tree
Hide file tree
Showing 10 changed files with 762 additions and 80 deletions.
2 changes: 1 addition & 1 deletion examples/with-shadcn/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { NextRequest } from "next/server"
import { auth0 } from "./lib/auth0"

export async function middleware(request: NextRequest) {
return await auth0.middleware(request)
return await auth0.middleware(request);
}

export const config = {
Expand Down
2 changes: 1 addition & 1 deletion examples/with-shadcn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@auth0/nextjs-auth0": "^4.0.0",
"@auth0/nextjs-auth0": "^4.0.1",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
Expand Down
10 changes: 5 additions & 5 deletions examples/with-shadcn/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,46 @@ export class AccessTokenError extends SdkError {
this.code = code;
}
}

/**
* Enum representing error codes related to federated connection access tokens.
*/
export enum FederatedConnectionAccessTokenErrorCode {
/**
* The session is missing.
*/
MISSING_SESSION = "missing_session",

/**
* The refresh token is missing.
*/
MISSING_REFRESH_TOKEN = "missing_refresh_token",

/**
* Failed to exchange the refresh token.
*/
FAILED_TO_EXCHANGE = "failed_to_exchange_refresh_token"
}

/**
* Error class representing an access token error for federated connections.
* Extends the `SdkError` class.
*/
export class FederatedConnectionsAccessTokenError extends SdkError {
/**
* The error code associated with the access token error.
*/
public code: string;

/**
* Constructs a new `FederatedConnectionsAccessTokenError` instance.
*
* @param code - The error code.
* @param message - The error message.
*/
constructor(code: string, message: string) {
super(message);
this.name = "FederatedConnectionAccessTokenError";
this.code = code;
}
}
225 changes: 225 additions & 0 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4153,6 +4153,231 @@ ca/T0LLtgmbMmxSv/MmzIg==
});
});
});

describe("getFederatedConnectionTokenSet", async () => {
it("should call for an access token when no federated connection token set in the session", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret
});
const sessionStore = new StatelessSessionStore({
secret
});
const fetchSpy = getMockAuthorizationServer({
tokenEndpointResponse: {
token_type: "Bearer",
access_token: DEFAULT.accessToken,
expires_in: 86400 // expires in 10 days
} as oauth.TokenEndpointResponse
});

const authClient = new AuthClient({
transactionStore,
sessionStore,

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

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: fetchSpy
});

const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; // expired 10 days ago
const tokenSet = {
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt
};

const response = await authClient.getFederatedConnectionTokenSet(
tokenSet,
undefined,
{ connection: "google-oauth2", login_hint: "000100123" }
);
const [error, federatedConnectionTokenSet] = response;
expect(error).toBe(null);
expect(fetchSpy).toHaveBeenCalled();
expect(federatedConnectionTokenSet).toEqual({
accessToken: DEFAULT.accessToken,
connection: "google-oauth2",
expiresAt: expect.any(Number)
});
});

it("should return access token from the session when federated connection token set in the session is not expired", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret
});
const sessionStore = new StatelessSessionStore({
secret
});
const fetchSpy = vi.fn();
const authClient = new AuthClient({
transactionStore,
sessionStore,

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

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: fetchSpy
});

const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; // expired 10 days ago
const tokenSet = {
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt,
};

const response = await authClient.getFederatedConnectionTokenSet(
tokenSet,
{ connection: 'google-oauth2', accessToken: 'fc_at', expiresAt: Math.floor(Date.now() / 1000) + 86400 },
{ connection: "google-oauth2", login_hint: "000100123" }
);
const [error, federatedConnectionTokenSet] = response;
expect(error).toBe(null);
expect(federatedConnectionTokenSet).toEqual({
accessToken: 'fc_at',
connection: "google-oauth2",
expiresAt: expect.any(Number)
});
expect(fetchSpy).not.toHaveBeenCalled();
});

it("should call for an access token when federated connection token set in the session is expired", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret
});
const sessionStore = new StatelessSessionStore({
secret
});
const fetchSpy = getMockAuthorizationServer({
tokenEndpointResponse: {
token_type: "Bearer",
access_token: DEFAULT.accessToken,
expires_in: 86400 // expires in 10 days
} as oauth.TokenEndpointResponse
});
const authClient = new AuthClient({
transactionStore,
sessionStore,

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

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: fetchSpy
});

const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; // expired 10 days ago
const tokenSet = {
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt,
};

const response = await authClient.getFederatedConnectionTokenSet(
tokenSet,
{ connection: 'google-oauth2', accessToken: 'fc_at', expiresAt },
{ connection: "google-oauth2", login_hint: "000100123" }
);
const [error, federatedConnectionTokenSet] = response;
expect(error).toBe(null);
expect(federatedConnectionTokenSet).toEqual({
accessToken: DEFAULT.accessToken,
connection: "google-oauth2",
expiresAt: expect.any(Number)
});
expect(fetchSpy).toHaveBeenCalled();
});

it("should return an error if the discovery endpoint could not be fetched", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret
});
const sessionStore = new StatelessSessionStore({
secret
});
const authClient = new AuthClient({
transactionStore,
sessionStore,

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

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: getMockAuthorizationServer({
discoveryResponse: new Response(null, { status: 500 })
})
});

const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; // expired 10 days ago
const tokenSet = {
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt
};

const [error, federatedConnectionTokenSet] =
await authClient.getFederatedConnectionTokenSet(tokenSet, undefined, {
connection: "google-oauth2"
});
expect(error?.code).toEqual("discovery_error");
expect(federatedConnectionTokenSet).toBeNull();
});

it("should return an error if the token set does not contain a refresh token", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret
});
const sessionStore = new StatelessSessionStore({
secret
});
const authClient = new AuthClient({
transactionStore,
sessionStore,

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

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: getMockAuthorizationServer()
});

const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; // expired 10 days ago
const tokenSet = {
accessToken: DEFAULT.accessToken,
expiresAt
};

const [error, federatedConnectionTokenSet] =
await authClient.getFederatedConnectionTokenSet(tokenSet, undefined, {
connection: "google-oauth2"
});
expect(error?.code).toEqual("missing_refresh_token");
expect(federatedConnectionTokenSet).toBeNull();
});
});
});

const _authorizationServerMetadata = {
Expand Down
Loading

0 comments on commit 7a253a2

Please sign in to comment.