Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/config/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,13 @@ export interface ListViewConfig {
rowSelectionView?: ComponentsConfig;
}

export enum OAUTH_FLOW {
AUTHORIZATION_CODE = "AUTHORIZATION_CODE",
IMPLICIT = "IMPLICIT",
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Use of `any` is intentional to allow for flexibility in the model.
export interface OAuthProvider<P = any> {
interface OAuthProviderBase<P = any> {
authorization: { params: { scope: string } };
clientId: string;
icon: ReactNode;
Expand All @@ -293,6 +298,24 @@ export interface OAuthProvider<P = any> {
userinfo: string;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Use of `any` is intentional to allow for flexibility in the model.
export interface ImplicitFlowProvider<P = any> extends OAuthProviderBase<P> {
flow: OAUTH_FLOW.IMPLICIT;
}

export interface AuthorizationCodeFlowProvider<
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Use of `any` is intentional to allow for flexibility in the model.
P = any,
> extends OAuthProviderBase<P> {
authorize: string; // Backend endpoint that exchanges an OAuth authorization code for a token set.
flow: OAUTH_FLOW.AUTHORIZATION_CODE;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Use of `any` is intentional to allow for flexibility in the model.
export type OAuthProvider<P = any> =
| ImplicitFlowProvider<P>
| AuthorizationCodeFlowProvider<P>;

/**
* Option Method.
*/
Expand Down
70 changes: 13 additions & 57 deletions src/google/service.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,31 @@
import { requestAuth, resetAuthState } from "../auth/dispatch/auth";
import {
requestAuthentication,
resetAuthenticationState,
updateAuthentication,
} from "../auth/dispatch/authentication";
import { resetCredentialsState } from "../auth/dispatch/credentials";
import { resetTokenState, updateToken } from "../auth/dispatch/token";
import { AUTHENTICATION_STATUS } from "../auth/types/authentication";
import { OAuthProvider } from "../config/entities";
import { GoogleProfile, SessionDispatch, TokenSetParameters } from "./types";
import { fetchProfile, getAuthenticationRequestOptions } from "./utils/auth";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO see https://github.com/clevercanary/data-browser/issues/544.
declare const google: any;
import { OAUTH_FLOW, type OAuthProvider } from "../config/entities";
import { login as authorizationCodeFlowLogin } from "./services/authorizationCodeFlow";
import { logout, type LoginDispatch } from "./services/common";
import { login as implicitFlowLogin } from "./services/implicitFlow";
import type { SessionDispatch } from "./types";

/**
* Google Sign-In service.
*/
export const service = {
/**
* Login with Google OAuth.
* Dispatches to the configured flow based on `provider.flow`.
* @param provider - OAuth provider configuration.
* @param dispatch - Dispatch functions for auth state.
*/
login: (
provider: OAuthProvider,
dispatch: Pick<
SessionDispatch,
"authDispatch" | "authenticationDispatch" | "tokenDispatch"
>,
): void => {
const client = google.accounts.oauth2.initTokenClient({
callback: (response: TokenSetParameters) => {
const { id, profile, userinfo } = provider;
const { access_token: token } = response;
dispatch.authDispatch?.(requestAuth());
dispatch.authenticationDispatch?.(requestAuthentication());
dispatch.tokenDispatch?.(updateToken({ providerId: id, token }));
fetchProfile(userinfo, getAuthenticationRequestOptions(token), {
onError: () => {
dispatch.authDispatch?.(resetAuthState());
dispatch.authenticationDispatch?.(
updateAuthentication({
profile: undefined,
status: AUTHENTICATION_STATUS.SETTLED,
}),
);
dispatch.tokenDispatch?.(resetTokenState());
},
onSuccess: (r: GoogleProfile) =>
dispatch.authenticationDispatch?.(
updateAuthentication({
profile: profile(r),
status: AUTHENTICATION_STATUS.PENDING, // Authentication is pending until session controller is resolved.
}),
),
});
},
client_id: provider.clientId,
scope: provider.authorization.params.scope,
});
client.requestAccessToken();
login: (provider: OAuthProvider, dispatch: LoginDispatch): void => {
if (provider.flow === OAUTH_FLOW.AUTHORIZATION_CODE) {
authorizationCodeFlowLogin(provider, dispatch);
} else {
implicitFlowLogin(provider, dispatch);
}
},
/**
* Logout and clear all auth state.
* @param dispatch - Dispatch functions for auth state.
*/
logout: (dispatch: SessionDispatch): void => {
Comment thread
frano-m marked this conversation as resolved.
dispatch.authDispatch?.(resetAuthState());
dispatch.authenticationDispatch?.(resetAuthenticationState());
dispatch.credentialsDispatch?.(resetCredentialsState());
dispatch.tokenDispatch?.(resetTokenState());
logout(dispatch);
},
};
51 changes: 51 additions & 0 deletions src/google/services/authorizationCodeFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { AuthorizationCodeFlowProvider } from "../../config/entities";
import type { CodeResponse, TokenSetParameters } from "../types";
import {
createOnAccessToken,
createResetSession,
type LoginDispatch,
} from "./common";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO see https://github.com/clevercanary/data-browser/issues/544.
declare const google: any;

/**
* Login using the OAuth 2.0 authorization code flow.
* Uses Google's initCodeClient to request an authorization code,
* then exchanges it for an access token via the configured authorize endpoint.
* @param provider - OAuth provider configuration.
* @param dispatch - Dispatch functions for auth state.
Comment thread
frano-m marked this conversation as resolved.
*/
export function login(
provider: AuthorizationCodeFlowProvider,
dispatch: LoginDispatch,
): void {
const { authorize } = provider;
const resetSession = createResetSession(dispatch);
const onAccessToken = createOnAccessToken(provider, dispatch, resetSession);
const client = google.accounts.oauth2.initCodeClient({
callback: (response: CodeResponse) => {
Comment thread
frano-m marked this conversation as resolved.
fetch(authorize, {
body: JSON.stringify(response),
headers: { "Content-Type": "application/json" },
method: "POST",
})
.then((r) => {
if (!r.ok) {
throw new Error(`authorize request failed (${r.status})`);
}
return r.json();
})
.then((tokens: TokenSetParameters) => {
if (!tokens?.access_token) {
throw new Error("authorize response missing access_token");
}
onAccessToken(tokens.access_token);
})
.catch(resetSession);
},
client_id: provider.clientId,
scope: provider.authorization.params.scope,
});
client.requestCode();
}
77 changes: 77 additions & 0 deletions src/google/services/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { requestAuth, resetAuthState } from "../../auth/dispatch/auth";
import {
requestAuthentication,
resetAuthenticationState,
updateAuthentication,
} from "../../auth/dispatch/authentication";
import { resetCredentialsState } from "../../auth/dispatch/credentials";
import { resetTokenState, updateToken } from "../../auth/dispatch/token";
import { AUTHENTICATION_STATUS } from "../../auth/types/authentication";
import type { OAuthProvider } from "../../config/entities";
import type { GoogleProfile, SessionDispatch } from "../types";
import { fetchProfile, getAuthenticationRequestOptions } from "../utils/auth";

export type LoginDispatch = Pick<
SessionDispatch,
"authDispatch" | "authenticationDispatch" | "tokenDispatch"
>;

/**
* Creates a function that resets the session state on auth failure.
* @param dispatch - Dispatch functions for auth state.
* @returns reset session function.
*/
export function createResetSession(dispatch: LoginDispatch): () => void {
return (): void => {
dispatch.authDispatch?.(resetAuthState());
dispatch.authenticationDispatch?.(
updateAuthentication({
profile: undefined,
status: AUTHENTICATION_STATUS.SETTLED,
}),
);
dispatch.tokenDispatch?.(resetTokenState());
};
}

/**
* Creates a function that handles a successful access token.
* Dispatches auth state updates and fetches the user profile.
* @param provider - OAuth provider configuration.
* @param dispatch - Dispatch functions for auth state.
* @param resetSession - Reset session function.
* @returns on access token function.
*/
export function createOnAccessToken(
provider: OAuthProvider,
dispatch: LoginDispatch,
resetSession: () => void,
): (token: string) => void {
const { id, profile, userinfo } = provider;
return (token: string): void => {
dispatch.authDispatch?.(requestAuth());
dispatch.authenticationDispatch?.(requestAuthentication());
dispatch.tokenDispatch?.(updateToken({ providerId: id, token }));
fetchProfile(userinfo, getAuthenticationRequestOptions(token), {
onError: resetSession,
onSuccess: (r: GoogleProfile) =>
dispatch.authenticationDispatch?.(
updateAuthentication({
Comment on lines +55 to +59
profile: profile(r),
status: AUTHENTICATION_STATUS.PENDING,
}),
),
});
};
}

/**
* Logout and clear all auth state.
* @param dispatch - Dispatch functions for auth state.
Comment thread
frano-m marked this conversation as resolved.
*/
export function logout(dispatch: SessionDispatch): void {
dispatch.authDispatch?.(resetAuthState());
dispatch.authenticationDispatch?.(resetAuthenticationState());
dispatch.credentialsDispatch?.(resetCredentialsState());
dispatch.tokenDispatch?.(resetTokenState());
}
31 changes: 31 additions & 0 deletions src/google/services/implicitFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ImplicitFlowProvider } from "../../config/entities";
import type { TokenSetParameters } from "../types";
import {
createOnAccessToken,
createResetSession,
type LoginDispatch,
} from "./common";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO see https://github.com/clevercanary/data-browser/issues/544.
declare const google: any;

/**
* Login using the OAuth 2.0 implicit flow.
* Uses Google's initTokenClient to request an access token directly.
* @param provider - OAuth provider configuration.
* @param dispatch - Dispatch functions for auth state.
Comment thread
frano-m marked this conversation as resolved.
*/
export function login(
provider: ImplicitFlowProvider,
dispatch: LoginDispatch,
): void {
const resetSession = createResetSession(dispatch);
const onAccessToken = createOnAccessToken(provider, dispatch, resetSession);
const client = google.accounts.oauth2.initTokenClient({
callback: (response: TokenSetParameters) =>
onAccessToken(response.access_token),
Comment on lines +25 to +26
client_id: provider.clientId,
scope: provider.authorization.params.scope,
});
client.requestAccessToken();
}
12 changes: 12 additions & 0 deletions src/google/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ import {
TokenState,
} from "../auth/types/token";

/**
* Authorization code response from Google OAuth.
*/
export interface CodeResponse {
code?: string;
error?: string;
error_description?: string;
error_uri?: string;
scope?: string;
state?: string;
}

/**
* Google Sign-In authentication provider props.
*/
Expand Down
Loading
Loading