mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-31 17:19:56 +08:00
@@ -46,8 +46,9 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let endpoint = try call.getStringEnsure("endpoint")
|
||||
let code = try call.getStringEnsure("code")
|
||||
let state = try call.getStringEnsure("state")
|
||||
let clientNonce = call.getString("clientNonce")
|
||||
|
||||
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/oauth/callback", headers: [:], body: ["code": code, "state": state])
|
||||
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/oauth/callback", headers: [:], body: ["code": code, "state": state, "client_nonce": clientNonce])
|
||||
|
||||
if response.statusCode >= 400 {
|
||||
if let textBody = String(data: data, encoding: .utf8) {
|
||||
|
||||
@@ -177,11 +177,12 @@ framework.scope(ServerScope).override(AuthProvider, resolver => {
|
||||
});
|
||||
await writeEndpointToken(endpoint, token);
|
||||
},
|
||||
async signInOauth(code, state, _provider) {
|
||||
async signInOauth(code, state, _provider, clientNonce) {
|
||||
const { token } = await Auth.signInOauth({
|
||||
endpoint,
|
||||
code,
|
||||
state,
|
||||
clientNonce,
|
||||
});
|
||||
await writeEndpointToken(endpoint, token);
|
||||
return {};
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface AuthPlugin {
|
||||
endpoint: string;
|
||||
code: string;
|
||||
state: string;
|
||||
clientNonce?: string;
|
||||
}): Promise<{ token: string }>;
|
||||
signInPassword(options: {
|
||||
endpoint: string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -6,7 +6,8 @@ export interface AuthProvider {
|
||||
signInOauth(
|
||||
code: string,
|
||||
state: string,
|
||||
provider: string
|
||||
provider: string,
|
||||
clientNonce?: string
|
||||
): Promise<{ redirectUri?: string }>;
|
||||
|
||||
signInPassword(credential: {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user