Replies: 2 comments
-
Is on the roadmap! I'm currently doing some outside jobs that use this library so soon will be updated |
Beta Was this translation helpful? Give feedback.
0 replies
-
I've copied it and updated it internally, if that's any help: Auth0Strategy.server.ts: import type { Strategy } from 'remix-auth/strategy';
import { OAuth2Strategy } from 'remix-auth-oauth2';
/**
* @see https://auth0.com/docs/get-started/apis/scopes/openid-connect-scopes#standard-claims
*/
type Auth0Scopes = Array<string | 'email' | 'openid' | 'profile'>;
export interface Auth0StrategyOptions
extends Pick<OAuth2Strategy.ConstructorOptions, 'clientId' | 'clientSecret' | 'redirectURI' | 'scopes'> {
/**
* OAuth2 strategy options
*/
scopes?: Auth0Scopes;
/**
* Auth0 strategy options
*/
domain: string;
audience?: string;
organization?: string;
invitation?: string;
connection?: string;
}
export const Auth0StrategyDefaultName = 'auth0';
export class Auth0Strategy<User> extends OAuth2Strategy<User> {
public name = Auth0StrategyDefaultName;
private scopes: Auth0Scopes;
private readonly audience?: string;
private readonly organization?: string;
private readonly invitation?: string;
private readonly connection?: string;
public constructor(
options: Auth0StrategyOptions,
verify: Strategy.VerifyFunction<User, OAuth2Strategy.VerifyOptions>,
) {
super(
{
authorizationEndpoint: `https://${options.domain}/authorize`,
tokenEndpoint: `https://${options.domain}/oauth/token`,
tokenRevocationEndpoint: `https://${options.domain}/oauth/revoke`,
clientId: options.clientId,
clientSecret: options.clientSecret,
redirectURI: options.redirectURI,
},
verify,
);
this.scopes = options.scopes ?? ['openid', 'profile', 'email'];
this.audience = options.audience;
this.organization = options.organization;
this.invitation = options.invitation;
this.connection = options.connection;
}
protected authorizationParams(params: URLSearchParams, request: Request) {
params.set('scope', this.scopes.join(' '));
if (this.audience) {
params.set('audience', this.audience);
}
if (this.organization) {
params.set('organization', this.organization);
}
if (this.invitation) {
params.set('invitation', this.invitation);
}
if (this.connection) {
params.set('connection', this.connection);
}
// Add additional search params
const requestSearchParams = new URL(request.url).searchParams;
for (const [key, value] of requestSearchParams) {
params.set(key, value);
}
return params;
}
} Auth0Strategy.server.test.ts: import { SetCookie } from '@mjackson/headers';
import { http, HttpResponse } from 'msw';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Auth0Strategy } from '~/services/Auth0Strategy.server';
import { server } from '~/test/mocks/api/server';
describe(Auth0Strategy, () => {
const verify = vi.fn();
beforeEach(() => {
vi.resetAllMocks();
server.resetHandlers();
});
it('should allow changing the scope', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
scopes: ['custom'],
},
verify,
);
const request = new Request('https://example.app/auth/auth0');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.searchParams.get('scope')).toBe('custom');
}
});
it('should have the scope `openid profile email` as default', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
},
verify,
);
const request = new Request('https://example.app/auth/auth0');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.searchParams.get('scope')).toBe('openid profile email');
}
});
it('should correctly format the authorization URL', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
},
verify,
);
const request = new Request('https://example.app/auth/auth0');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.hostname).toBe('test.fake.auth0.com');
expect(redirectUrl.pathname).toBe('/authorize');
}
});
it('should allow changing the audience', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
scopes: ['custom'],
audience: 'SOME_AUDIENCE',
},
verify,
);
const request = new Request('https://example.app/auth/auth0');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.searchParams.get('audience')).toBe('SOME_AUDIENCE');
}
});
it('should allow changing the organization', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
scopes: ['custom'],
audience: 'SOME_AUDIENCE',
organization: 'SOME_ORG',
},
verify,
);
const request = new Request('https://example.app/auth/auth0');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.searchParams.get('organization')).toBe('SOME_ORG');
}
});
it('should allow setting an invitation', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
scopes: ['custom'],
audience: 'SOME_AUDIENCE',
organization: 'SOME_ORG',
invitation: 'SOME_INVITATION',
},
verify,
);
const request = new Request('https://example.app/auth/auth0');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.searchParams.get('invitation')).toBe('SOME_INVITATION');
}
});
it('should allow setting the connection type', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
scopes: ['custom'],
audience: 'SOME_AUDIENCE',
organization: 'SOME_ORG',
connection: 'email',
},
verify,
);
const request = new Request('https://example.app/auth/auth0');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.searchParams.get('connection')).toBe('email');
}
});
it('should not fetch user profile when openid scope is not present', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
scopes: ['custom'],
},
verify,
);
const header = new SetCookie({
name: 'oauth2',
value: new URLSearchParams({ state: 'random-state', codeVerifier: 'random-code-verifier' }).toString(),
httpOnly: true, // Prevents JavaScript from accessing the cookie
maxAge: 60 * 5, // 5 minutes
path: '/', // Allow the cookie to be sent to any path
sameSite: 'Lax', // Prevents it from being sent in cross-site requests
});
const request = new Request('https://example.com/callback?state=random-state&code=random-code', {
headers: { cookie: header.toString() },
});
server.use(
http.post(
'https://test.fake.auth0.com/oauth/token',
() =>
HttpResponse.json({
access_token: 'access token',
scope: 'custom',
expires_in: 86_400,
token_type: 'Bearer',
}),
{ once: true },
),
);
await strategy.authenticate(request);
expect(verify).toHaveBeenLastCalledWith({
tokens: {
data: {
access_token: 'access token',
expires_in: 86_400,
scope: 'custom',
token_type: 'Bearer',
},
},
request,
});
});
it('should allow additional search params', async () => {
const strategy = new Auth0Strategy(
{
domain: 'test.fake.auth0.com',
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectURI: 'https://example.app/callback',
},
verify,
);
const request = new Request('https://example.app/auth/auth0?test=1');
try {
await strategy.authenticate(request);
} catch (error) {
if (!(error instanceof Response)) throw error;
const location = error.headers.get('Location');
if (!location) throw new Error('No redirect header');
const redirectUrl = new URL(location);
expect(redirectUrl.searchParams.get('test')).toBe('1');
}
});
}); |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Are there any plans to update to Remix Auth v4? this adds support for RR7, but I think there are quite some breaking changes; sergiodxa/remix-auth#299
Beta Was this translation helpful? Give feedback.
All reactions