refactor(server): improve oauth login flow (#10648)

close CLOUD-145
This commit is contained in:
fengmk2
2025-03-12 06:53:29 +00:00
parent d823792f85
commit 867ae7933f
16 changed files with 211 additions and 31 deletions

View File

@@ -1,11 +1,15 @@
import { Button } from '@affine/component/ui/button';
import { ServerService } from '@affine/core/modules/cloud';
import { notify } from '@affine/component/ui/notification';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { AuthService, ServerService } from '@affine/core/modules/cloud';
import { UrlService } from '@affine/core/modules/url';
import { type UserFriendlyError } from '@affine/error';
import { OAuthProviderType } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { GithubIcon, GoogleIcon, LockIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { type ReactElement, type SVGAttributes, useCallback } from 'react';
import { type ReactElement, type SVGAttributes } from 'react';
const OAuthProviderMap: Record<
OAuthProviderType,
@@ -64,9 +68,27 @@ function OAuthProvider({
popupWindow: (url: string) => void;
}) {
const serverService = useService(ServerService);
const auth = useService(AuthService);
const { icon } = OAuthProviderMap[provider];
const t = useI18n();
const onClick = useAsyncCallback(async () => {
if (scheme && BUILD_CONFIG.isNative) {
let oauthUrl = '';
try {
oauthUrl = await auth.oauthPreflight(provider, scheme);
} catch (e) {
console.error(e);
const err = e as UserFriendlyError;
notify.error({
title: t[`error.${err.name}`](err.data),
});
return;
}
popupWindow(oauthUrl);
return;
}
const onClick = useCallback(() => {
const params = new URLSearchParams();
params.set('provider', provider);
@@ -88,7 +110,7 @@ function OAuthProvider({
track.$.$.auth.signIn({ method: 'oauth', provider });
popupWindow(oauthUrl);
}, [popupWindow, provider, redirectUrl, scheme, serverService]);
}, [popupWindow, provider, redirectUrl, scheme, serverService, auth, t]);
return (
<Button

View File

@@ -18,10 +18,15 @@ export function configureDefaultAuthProvider(framework: Framework) {
});
},
async signInOauth(code: string, state: string, _provider: string) {
async signInOauth(
code: string,
state: string,
_provider: string,
clientNonce?: string
) {
const res = await fetchService.fetch('/api/oauth/callback', {
method: 'POST',
body: JSON.stringify({ code, state }),
body: JSON.stringify({ code, state, client_nonce: clientNonce }),
headers: {
'content-type': 'application/json',
},

View File

@@ -6,7 +6,8 @@ export interface AuthProvider {
signInOauth(
code: string,
state: string,
provider: string
provider: string,
clientNonce?: string
): Promise<{ redirectUri?: string }>;
signInPassword(credential: {

View File

@@ -3,6 +3,7 @@ import { UserFriendlyError } from '@affine/error';
import type { OAuthProviderType } from '@affine/graphql';
import { track } from '@affine/track';
import { OnEvent, Service } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { distinctUntilChanged, map, skip } from 'rxjs';
import { ApplicationFocused } from '../../lifecycle';
@@ -130,10 +131,16 @@ export class AuthService extends Service {
client: string,
/** @deprecated*/ redirectUrl?: string
) {
this.setClientNonce();
try {
const res = await this.fetchService.fetch('/api/oauth/preflight', {
method: 'POST',
body: JSON.stringify({ provider, redirect_uri: redirectUrl }),
body: JSON.stringify({
provider,
client,
redirect_uri: redirectUrl,
client_nonce: this.store.getClientNonce(),
}),
headers: {
'content-type': 'application/json',
},
@@ -141,19 +148,6 @@ export class AuthService extends Service {
let { url } = await res.json();
// change `state=xxx` to `state={state:xxx,native:true}`
// so we could know the callback should be redirect to native app
const oauthUrl = new URL(url);
oauthUrl.searchParams.set(
'state',
JSON.stringify({
state: oauthUrl.searchParams.get('state'),
client,
provider,
})
);
url = oauthUrl.toString();
return url as string;
} catch (e) {
track.$.$.auth.signInFail({
@@ -228,4 +222,11 @@ export class AuthService extends Service {
return headers;
}
private setClientNonce() {
if (BUILD_CONFIG.isNative) {
// send random client nonce on native app
this.store.setClientNonce(nanoid());
}
}
}

View File

@@ -48,6 +48,14 @@ export class AuthStore extends Store {
this.globalState.set(`${this.serverService.server.id}-auth`, session);
}
getClientNonce() {
return this.globalState.get<string>('auth-client-nonce');
}
setClientNonce(nonce: string) {
this.globalState.set('auth-client-nonce', nonce);
}
async fetchSession() {
const url = `/api/auth/session`;
const options: RequestInit = {
@@ -70,7 +78,12 @@ export class AuthStore extends Store {
}
async signInOauth(code: string, state: string, provider: string) {
return await this.authProvider.signInOauth(code, state, provider);
return await this.authProvider.signInOauth(
code,
state,
provider,
this.getClientNonce()
);
}
async signInPassword(credential: {