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

[SDK-4319] Add support for Edge runtime #1269

Merged
merged 3 commits into from
Jun 28, 2023
Merged

Conversation

adamjmcgrath
Copy link
Contributor

📋 Changes

Have added support for the Edge runtime (in the app directory) to the auth handlers and session helpers:

Auth Handlers
handleAuth
handleLogin
handleCallback
handleLogout
handleProfile

Session Helpers
withApiAuthRequired
withPageAuthRequired
updateSession
getAccessToken
(getSession is already supported)

We now use 2 OIDC clients:

Using Jest "projects" to run the handler tests in both runtimes
Have updated the example app to use edge as well as node
Also support added for middleware responses (see #1150)

📎 References

Fixes #912 #1150 #1257

🎯 Testing

There is an example app running on the edge runtime on Vercel https://app-dir-auth0.vercel.app/ (source)

@adamjmcgrath adamjmcgrath added the review:large Large review label Jun 26, 2023
@adamjmcgrath adamjmcgrath requested a review from a team as a code owner June 26, 2023 14:44
@vercel
Copy link

vercel bot commented Jun 26, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Ignored Deployment
Name Status Preview Comments Updated (UTC)
nextjs-auth0 ⬜️ Ignored (Inspect) Jun 28, 2023 11:20am

@adamjmcgrath adamjmcgrath changed the title Add support for Edge runtime [SDK-4319] Add support for Edge runtime Jun 26, 2023
@@ -41,28 +41,52 @@ export default function Nav() {
<a>Profile</a>
</Link>
</li>
<li>
<Link href="/edge-profile" legacyBehavior>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is legacyBehavior still needed here (and in other links)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just moving this over from the old example app, I could change them - but I'd rather do that in another PR

return auth0LogoutUrl.toString();
}
if (!as.end_session_endpoint) {
throw new Error('no rp inititated logout');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this error have a more specific type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of our configuration errors have a type, they just need to tell the developer they've configured the SDK wrong

Copy link
Contributor

@Widcket Widcket Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this error in particular be a little more descriptive then? If I were a developer new to identity and got that error, I think I'd be confused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - I can improve the message

if (clientPrivateKey && !(clientPrivateKey instanceof CryptoKey)) {
clientPrivateKey = await jose.importPKCS8<CryptoKey>(clientPrivateKey, clientAssertionSigningAlg || 'RS256');
}
const response = await oauth.authorizationCodeGrantRequest(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this call throw an exception? If so, should it be caught, wrapped, and rethrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the node handler we're wrapping the errors in our own, more specific error types:

throw new IdentityProviderError(err);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authorizationCodeGrantRequest doesn't throw an IdentityProvider error (one with an error / error_description that comes from the IDP) it just returns a fetch response (or throws config errors like, "you forgot the response_type")

processAuthorizationCodeOpenIDResponse returns an IDP error (not throws) which is thrown on L158

}
);

const result = await oauth.processAuthorizationCodeOpenIDResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


async userinfo(accessToken: string): Promise<Record<string, unknown>> {
const [as, client] = await this.getClient();
const response = await oauth.userInfoRequest(as, client, accessToken, this.httpOptions());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this call throw an exception? If so, should it be caught, wrapped, and rethrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the node handler we're wrapping the errors in our own, more specific error types:

throw new IdentityProviderError(err);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for userinfo - but yeah - fair enough, I'll create a UserInfoError and add it to the node and edge clients


async refresh(refreshToken: string, extras: { exchangeBody: Record<string, any> }): Promise<TokenEndpointResponse> {
const [as, client] = await this.getClient();
const res = await oauth.refreshTokenGrantRequest(as, client, refreshToken, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this call throw an exception? If so, should it be caught, wrapped, and rethrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should throw an AccessTokenError will fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be missing in 0bb1e4a

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, was going to say - this is the same as #1269 (comment) the error from the idp get's thrown from processRefreshTokenResponse

additionalParameters: extras.exchangeBody,
...this.httpOptions()
});
const result = await oauth.processRefreshTokenResponse(as, client, res);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't throw, if there's an error oauth.isOAuth2Error(result) is true (which is caught below)

Comment on lines +85 to +88
throw new Error(`response_type should be one of ${validResponseTypes.join(', ')}`);
}
if (!/\bopenid\b/.test(authParams.scope as string)) {
throw new Error('scope should contain "openid"');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these errors have a more specific type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a configuration error too - we don't have a type for these. We could look at changing that, but it would be a lot of changes for not much/any benefit.

src/edge.ts Outdated
@@ -28,39 +29,57 @@ const genId = () => {
.join('');
};

function getInstance(params?: ConfigParameters): Auth0Edge {
const telemetry = { name: 'nextjs-auth0', version };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be moved to some common helper, as it's also used in

const telemetry = { name: 'nextjs-auth0', version };
?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can move this to shared.ts

@@ -110,7 +108,7 @@ export class AccessTokenError extends AuthError {
/**
* @ignore
*/
export type HandlerErrorCause = Error | AuthError | HttpError;
export type HandlerErrorCause = Error | AuthError | (Error & { status: number; statusCode: number });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Error & { status: number; statusCode: number } be its own type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - I'll make an HttpError type

@@ -436,60 +436,6 @@ describe('callback', () => {
expect(header.alg).toEqual('RS256');
});

it('should use client secret jwt on token endpoint', async () => {
Copy link
Contributor

@Widcket Widcket Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies if this is something obvious, why is this test being removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth0 doesn't support client secret jwt and the edge client doesn't so I've removed support

Comment on lines 30 to 32
} else {
return nodeInitAuth0(config);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else {
return nodeInitAuth0(config);
}
}
return nodeInitAuth0(config);

Comment on lines +65 to +67
if (this.as) {
return [this.as, this.client as oauth.Client];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how long-lived instances in the Edge runtime can be (apparently there's still some notion of "cold-start"). If these were short-lived, wouldn't we be calling the discovery endpoint a lot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't do any better than keeping them in memory 🤷 (unless Next.js has some built in distributed cache I haven't heard of)

Copy link
Contributor

@Widcket Widcket Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have an option for not using the discovery endpoint?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After all this is an Auth0-specific SDK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe not using it by default on the edge client.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asking because serverless !== edge. As Vercel's architect mentions, "Edge Functions bring another set of trade-offs into the mix".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was enough/serviceable with serverless might not be with edge functions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, this probably warrants more investigation and testing with the edge runtime to find out if something more needs to be done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since edge functions apparently use Cloudflare Workers, I guess it comes down to the differences between Cloudflare Workers and AWS Lambda.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This SDK is agnostic to a particular edge vendor (Vercel/AWS/Azure/CloudFlare/GCP) - but sure, we can look into how their memory persistence differs between the 2 runtimes. I guess at the very least, Edge functions will be more populous given that their are more Edge locations - so I guess the probability of hitting the same box is less. But on the other hand, the Discovery is served from a CDN, so it's not a huge cost.

describe('login handler (page router)', () => {
afterEach(teardown);

test('should create a state', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test('should create a state', async () => {
test('should create a state, nonce, and code verifier', async () => {

@adamjmcgrath
Copy link
Contributor Author

Thanks for the review @Widcket 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review:large Large review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants