feat(core): support apple sign in (#12424)

This commit is contained in:
liuyi
2025-05-23 15:27:27 +08:00
committed by GitHub
parent a96cd3eb0a
commit 41781902f6
22 changed files with 629 additions and 260 deletions
@@ -19,6 +19,7 @@ import {
OauthAccountAlreadyConnected,
OauthStateExpired,
UnknownOauthProvider,
URLHelper,
UseNamedGuard,
} from '../../base';
import { AuthService, Public } from '../../core/auth';
@@ -36,7 +37,8 @@ export class OAuthController {
private readonly auth: AuthService,
private readonly oauth: OAuthService,
private readonly models: Models,
private readonly providerFactory: OAuthProviderFactory
private readonly providerFactory: OAuthProviderFactory,
private readonly url: URLHelper
) {}
@Public()
@@ -67,10 +69,14 @@ export class OAuthController {
clientNonce,
});
const stateStr = JSON.stringify({
state,
client,
provider: unknownProviderName,
});
return {
url: provider.getAuthUrl(
JSON.stringify({ state, client, provider: unknownProviderName })
),
url: provider.getAuthUrl(stateStr, clientNonce),
};
}
@@ -85,6 +91,7 @@ export class OAuthController {
@Body('state') stateStr?: string,
@Body('client_nonce') clientNonce?: string
) {
// TODO(@forehalo): refactor and remove deprecated code in 0.23
if (!code) {
throw new MissingOauthQueryParameter({ name: 'code' });
}
@@ -93,6 +100,17 @@ export class OAuthController {
throw new MissingOauthQueryParameter({ name: 'state' });
}
// NOTE(@forehalo): Apple sign in will directly post /callback, with `state` set at #L73
let rawState = null;
if (typeof stateStr === 'string' && stateStr.length > 36) {
try {
rawState = JSON.parse(stateStr);
stateStr = rawState.state;
} catch {
/* noop */
}
}
if (typeof stateStr !== 'string' || !this.oauth.isValidState(stateStr)) {
throw new InvalidOauthCallbackState();
}
@@ -103,8 +121,38 @@ export class OAuthController {
throw new OauthStateExpired();
}
if (
state.provider === OAuthProviderName.Apple &&
rawState &&
state.client &&
state.client !== 'web'
) {
const clientUrl = new URL(`${state.client}://authentication`);
clientUrl.searchParams.set('method', 'oauth');
clientUrl.searchParams.set(
'payload',
JSON.stringify({
state: stateStr,
code,
provider: rawState.provider,
})
);
clientUrl.searchParams.set('server', this.url.origin);
return res.redirect(
this.url.link('/open-app/url?', {
url: clientUrl.toString(),
})
);
}
// TODO(@fengmk2): clientNonce should be required after the client version >= 0.21.0
if (state.clientNonce && state.clientNonce !== clientNonce) {
if (
state.clientNonce &&
state.clientNonce !== clientNonce &&
// apple sign in with nonce stored in id token
state.provider !== OAuthProviderName.Apple
) {
throw new InvalidAuthState();
}
@@ -132,7 +180,8 @@ export class OAuthController {
);
throw err;
}
const externAccount = await provider.getUser(tokens.accessToken);
const externAccount = await provider.getUser(tokens, state);
const user = await this.loginFromOauth(
state.provider,
externAccount,
@@ -140,6 +189,14 @@ export class OAuthController {
);
await this.auth.setCookies(req, res, user.id);
if (
state.provider === OAuthProviderName.Apple &&
(!state.client || state.client === 'web')
) {
return res.redirect(this.url.link(state.redirectUri ?? '/'));
}
res.send({
id: user.id,
redirectUri: state.redirectUri,
@@ -170,7 +227,9 @@ export class OAuthController {
userId: user.id,
provider,
providerAccountId: externalAccount.id,
...tokens,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
});
return user;
@@ -180,10 +239,11 @@ export class OAuthController {
connectedAccount: ConnectedAccount,
tokens: Tokens
) {
return await this.models.user.updateConnectedAccount(
connectedAccount.id,
tokens
);
return await this.models.user.updateConnectedAccount(connectedAccount.id, {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
});
}
/**
@@ -210,7 +270,9 @@ export class OAuthController {
userId: user.id,
provider,
providerAccountId: externalAccount.id,
...tokens,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
});
}
}