@webda/google-auth 4.0.0-beta.2 → 4.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # @webda/google-auth module
2
+
3
+ This module is part of Webda Application Framework that allows you to quickly develop applications with all modern prerequisites: Security, Extensibility, GraphQL, REST, CloudNative [https://webda.io](https://webda.io)
4
+
5
+ <img src="https://webda.io/images/webda.svg" width="128" />
6
+
7
+ ![CI](https://github.com/loopingz/webda.io/workflows/CI/badge.svg)
8
+
9
+ [![Join the chat at https://gitter.im/loopingz/webda](https://badges.gitter.im/loopingz/webda.svg)](https://gitter.im/loopingz/webda?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
10
+ [![codecov](https://codecov.io/gh/loopingz/webda.io/branch/main/graph/badge.svg?token=8N9DNM3K3O)](https://codecov.io/gh/loopingz/webda.io)
11
+ [![SonarCloud.io](https://sonarcloud.io/api/project_badges/measure?project=loopingz_webda.io&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=loopingz_webda.io)
12
+ ![CodeQL](https://github.com/loopingz/webda.io/workflows/CodeQL/badge.svg)
13
+
14
+ <!-- README_HEADER -->
15
+
16
+ # @webda/google-auth
17
+
18
+ > "Sign in with Google" (OpenID Connect) provider for [`@webda/auth`](../auth): adds `GET /auth/google`, its callback
19
+ > and the `Auth.Google.Token` operation; logins go through the `Authentication` service (linking, registration,
20
+ > sessions, tokens).
21
+
22
+ ## When to use it
23
+
24
+ - You want users to sign in with their Google account, in the browser or from a mobile/desktop client holding a
25
+ Google ID token.
26
+ - You want to restrict logins to a Google Workspace domain (`hostedDomain`).
27
+ - You need an offline Google refresh token (`access_type: "offline"`; stored in the ident tokens).
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pnpm add @webda/auth @webda/google-auth
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ `GoogleAuthentication` is a provider service next to `Authentication` (see the
38
+ [Authentication guide](https://docs.webda.io/Security/Authentication)):
39
+
40
+ ```jsonc
41
+ {
42
+ "services": {
43
+ "Authentication": { "type": "Webda/Authentication" },
44
+ "google": {
45
+ "type": "Webda/GoogleAuthentication",
46
+ "client_id": "${GOOGLE_CLIENT_ID}",
47
+ "client_secret": "${GOOGLE_CLIENT_SECRET}",
48
+ // Register exactly this url in the Google Cloud console; set it explicitly behind a proxy
49
+ "redirect_uri": "https://api.example.com/auth/google/callback",
50
+ "redirects": {
51
+ "success": "https://app.example.com/", // default "/"
52
+ "failure": "https://app.example.com/login" // required, receives ?reason=CODE
53
+ },
54
+ // Allowed targets of GET /auth/google?redirect=... (same origin, this path or below)
55
+ "authorized_uris": ["https://app.example.com/"],
56
+ "audiences": ["1234-ios.apps.googleusercontent.com"], // other client ids accepted by Auth.Google.Token
57
+ "hostedDomain": "example.com" // optional: Google Workspace accounts of this domain only
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ | Parameter | Default | Description |
64
+ | ----------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------- |
65
+ | `client_id`, `client_secret` | required | OAuth client of type "Web application" |
66
+ | `url` | `/auth/google` | Prefix of the login and callback routes |
67
+ | `redirect_uri` | request url + `/callback` | Callback url sent to Google |
68
+ | `redirects.success` / `redirects.failure` | `/` / required | Where the callback redirects |
69
+ | `authorized_uris` | `[]` | Allowed `redirect` parameters (absolute urls) |
70
+ | `scope` | `["openid", "email", "profile"]` | Requested scopes (`openid` is needed for the ID token) |
71
+ | `access_type` | `"online"` | `"offline"` also returns a refresh token |
72
+ | `audiences` | `[]` | Extra client ids accepted by `Auth.Google.Token` |
73
+ | `hostedDomain` | - | Required `hd` claim; refused with `EMAIL_DOMAIN_NOT_ALLOWED` |
74
+ | `auth_options` | - | Extra authorization url parameters (`prompt`, `login_hint`...); cannot override the flow fields |
75
+ | `allowedEmailDomains`, `trustEmailVerification` | - | Per-provider email policy of `@webda/auth` |
76
+
77
+ ## Usage
78
+
79
+ - Browser: link to `GET /auth/google?redirect=https://app.example.com/after`. The flow uses PKCE and an OpenID nonce,
80
+ kept in a short-lived encrypted `webda_oauth_google` cookie (so it also works with a `SameSite=Strict` session
81
+ cookie). After the Google consent the callback logs the user in and redirects to `redirect` (or
82
+ `redirects.success`), adding `?mfa=required` when the user still has to pass MFA. Failures redirect to
83
+ `redirects.failure?reason=CODE` (`STATE_MISMATCH`, `PROVIDER_ERROR`, `TOKEN_INVALID`, `ACCOUNT_EXISTS`,
84
+ `EMAIL_DOMAIN_NOT_ALLOWED`, ...).
85
+ - Other clients: `POST /auth/google/token` (`Auth.Google.Token`) with a JSON body `{ "token": "<Google ID token>" }`
86
+ or the v3 body `{ "tokens": { "id_token": "...", "access_token": "...", ... } }` returns the `@webda/auth` result
87
+ (`{ status: "ok", accessToken, refreshToken, ... }`). Only ID tokens are verified; access tokens alone are refused.
88
+ This operation never links the identity to an already logged-in user.
89
+
90
+ The Google identity is the ID token `sub`; the email is only treated as verified when `email_verified` is `true`.
91
+ Google credentials are stored encrypted on the ident (`await ident.tokens.get()`) and published after each login:
92
+
93
+ ```typescript
94
+ useService("google").on("GoogleAuth.Tokens", async ({ tokens, context }) => {
95
+ // tokens.access_token, tokens.refresh_token (access_type "offline")
96
+ });
97
+ ```
98
+
99
+ ## Reference
100
+
101
+ - Source: [`packages/google-auth`](https://github.com/loopingz/webda.io/tree/main/packages/google-auth)
102
+ - Related: [`@webda/auth`](../auth) (`OAuthProvider` base class, `Authentication` service).
103
+
104
+ <!-- README_FOOTER -->
105
+
106
+ ## Sponsors
107
+
108
+ <!--
109
+ Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](mailto:sponsor@webda.io)
110
+ -->
111
+
112
+ Arize AI is a machine learning observability and model monitoring platform. It helps you visualize, monitor, and explain your machine learning models. [Learn more](https://arize.com)
113
+
114
+ [<img src="https://arize.com/hubfs/arize/brand/arize-logomark-1.png" width="200">](https://arize.com)
115
+
116
+ Loopingz is a software development company that provides consulting and development services. [Learn more](https://loopingz.com)
117
+
118
+ [<img src="https://loopingz.com/images/logo.png" width="200">](https://loopingz.com)
119
+
120
+ Tellae is an innovative consulting firm specialized in cities transportation issues. We provide our clients, both public and private, with solutions to support your strategic and operational decisions. [Learn more](https://tellae.fr)
121
+
122
+ [<img src="https://tellae.fr/" width="200">](https://tellae.fr)
@@ -0,0 +1,125 @@
1
+ import { type AuthResult, type EventWithContext, type ResolvedIdentity } from "@webda/core";
2
+ import { type OAuthAuthorizationRequest, type OAuthCallbackRequest, OAuthProvider, OAuthProviderParameters, type OAuthTokenRequest } from "@webda/auth";
3
+ import { type Credentials, OAuth2Client, type TokenPayload } from "google-auth-library";
4
+ /** Emitted with the Google credentials of a successful login */
5
+ export interface EventGoogleOAuthToken extends EventWithContext {
6
+ /**
7
+ * Credentials: from the code exchange (browser flow), or as sent to `Auth.Google.Token` (`tokens`, or
8
+ * `{ id_token }` for a bare token). Only the ID token is verified: treat the other values as client-provided on
9
+ * the token operation
10
+ */
11
+ tokens: Credentials;
12
+ }
13
+ /** Events of GoogleAuthentication */
14
+ export type GoogleAuthEvents = {
15
+ "GoogleAuth.Tokens": EventGoogleOAuthToken;
16
+ };
17
+ /**
18
+ * Google login parameters
19
+ * https://developers.google.com/identity/protocols/oauth2/openid-connect
20
+ */
21
+ export declare class GoogleParameters extends OAuthProviderParameters {
22
+ /**
23
+ * Google OAuth client id (web application)
24
+ */
25
+ client_id: string;
26
+ /**
27
+ * Google OAuth client secret
28
+ */
29
+ client_secret: string;
30
+ /**
31
+ * Scopes requested from Google
32
+ * @default ["openid", "email", "profile"]
33
+ */
34
+ scope?: string[];
35
+ /**
36
+ * Type of access: "offline" also returns a refresh token (stored encrypted in the ident tokens)
37
+ * @default "online"
38
+ */
39
+ access_type?: "online" | "offline";
40
+ /**
41
+ * Other client ids whose ID tokens `Auth.Google.Token` accepts (for example Android/iOS client ids)
42
+ * @default []
43
+ */
44
+ audiences?: string[];
45
+ /**
46
+ * Only accept Google Workspace accounts of this domain (the `hd` claim of the ID token); also sent as a hint to
47
+ * the account chooser
48
+ */
49
+ hostedDomain?: string;
50
+ /**
51
+ * Additional authorization url parameters (for example `prompt`, `login_hint`); cannot override `state`,
52
+ * `redirect_uri`, `scope`, `access_type`, `response_type`, `code_challenge*`, `nonce` nor `hd`
53
+ * See https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters
54
+ */
55
+ auth_options?: Record<string, any>;
56
+ /**
57
+ * @param params - raw parameters
58
+ * @returns this
59
+ */
60
+ load(params?: any): this;
61
+ }
62
+ /**
63
+ * Sign in with Google (OpenID Connect)
64
+ *
65
+ * The browser flow (`GET /auth/google`, `GET /auth/google/callback`) uses PKCE and a nonce, exchanges the code and
66
+ * verifies the returned ID token for `client_id`. `Auth.Google.Token` accepts a Google ID token, as `token` or as
67
+ * the `id_token` of `tokens` (v3 body), whose audience is `client_id` or one of `audiences` (never an access
68
+ * token). Credentials are stored encrypted on the ident and emitted with `GoogleAuth.Tokens` after the login.
69
+ * @WebdaModda GoogleAuthentication
70
+ */
71
+ export declare class GoogleAuthentication<T extends GoogleParameters = GoogleParameters> extends OAuthProvider<T, GoogleAuthEvents> {
72
+ static Parameters: typeof GoogleParameters;
73
+ readonly providerName = "google";
74
+ /** Client verifying ID tokens: one per service so the Google certificates cache is reused */
75
+ protected verifier: OAuth2Client;
76
+ /** @override */
77
+ init(): Promise<this>;
78
+ /**
79
+ * @param redirectUri - callback url
80
+ * @returns a Google OAuth client for the authorization url and the code exchange
81
+ */
82
+ protected getClient(redirectUri?: string): OAuth2Client;
83
+ /**
84
+ * @param request - authorization request
85
+ * @returns the Google authorization url
86
+ */
87
+ getAuthorizationUrl(request: OAuthAuthorizationRequest): string;
88
+ /**
89
+ * Exchange the code with the PKCE verifier, verify the returned ID token for `client_id` and its nonce
90
+ * @param request - code exchange
91
+ * @returns the identity, with the credentials of the exchange
92
+ */
93
+ handleCallback(request: OAuthCallbackRequest): Promise<ResolvedIdentity>;
94
+ /**
95
+ * Verify a Google ID token sent by a client, as `token` or `tokens.id_token`
96
+ * @param request - token request
97
+ * @returns the identity, with the credentials sent in `tokens`
98
+ */
99
+ handleToken(request: OAuthTokenRequest): Promise<ResolvedIdentity>;
100
+ /**
101
+ * Emit `GoogleAuth.Tokens` once the login succeeded
102
+ * @param identity - the identity
103
+ * @param _result - the result
104
+ * @param source - browser callback or token operation
105
+ * @param request - the token operation request
106
+ */
107
+ protected onAuthenticated(identity: ResolvedIdentity, _result: AuthResult, source: "callback" | "token", request?: OAuthTokenRequest): Promise<void>;
108
+ /**
109
+ * Verify an ID token: signature, expiry, issuer (google-auth-library), audience and hosted domain
110
+ * @param idToken - ID token
111
+ * @param audiences - accepted audiences
112
+ * @returns the payload
113
+ * @throws TokenInvalid when the token does not verify
114
+ * @throws EmailDomainNotAllowed when `hostedDomain` is set and the token is not from that domain
115
+ */
116
+ protected verifyIdToken(idToken: string, audiences: string[]): Promise<TokenPayload>;
117
+ /**
118
+ * @param payload - verified ID token payload
119
+ * @param tokens - credentials to store
120
+ * @returns the identity
121
+ */
122
+ protected toIdentity(payload: TokenPayload, tokens?: Credentials): ResolvedIdentity;
123
+ }
124
+ export default GoogleAuthentication;
125
+ //# sourceMappingURL=google-auth.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-auth.service.d.ts","sourceRoot":"","sources":["../src/google-auth.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAA0B,MAAM,aAAa,CAAC;AACpH,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,aAAa,EACb,uBAAuB,EACvB,KAAK,iBAAiB,EAGvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,WAAW,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE7G,gEAAgE;AAChE,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,qCAAqC;AACrC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mBAAmB,EAAE,qBAAqB,CAAC;CAC5C,CAAC;AAEF;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,uBAAuB;IAC3D;;OAEG;IACK,SAAS,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACK,aAAa,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACK,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACnC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEnC;;;OAGG;IACH,IAAI,CAAC,MAAM,GAAE,GAAQ,GAAG,IAAI,CAM3B;CACF;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB,CAAE,SAAQ,aAAa,CACpG,CAAC,EACD,gBAAgB,CACjB;IACC,MAAM,CAAC,UAAU,0BAAoB;IAErC,QAAQ,CAAC,YAAY,YAAY;IAEjC,6FAA6F;IAC7F,SAAS,CAAC,QAAQ,EAAE,YAAY,CAAC;IAEjC,gBAAgB;IACV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAI1B;IAED;;;OAGG;IACH,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,CAMtD;IAED;;;OAGG;IACH,mBAAmB,CAAC,OAAO,EAAE,yBAAyB,GAAG,MAAM,CAc9D;IAED;;;;OAIG;IACG,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoB7E;IAED;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAcvE;IAED;;;;;;OAMG;IACH,UAAgB,eAAe,CAC7B,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,UAAU,GAAG,OAAO,EAC5B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAGf;IAED;;;;;;;OAOG;IACH,UAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CA2BzF;IAED;;;;OAIG;IACH,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,gBAAgB,CAUlF;CACF;eAcc,oBAAoB"}
@@ -0,0 +1,197 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { useContext, WebdaError } from "@webda/core";
3
+ import { EmailDomainNotAllowed, OAuthProvider, OAuthProviderParameters, safeErrorReason, TokenInvalid } from "@webda/auth";
4
+ import { CodeChallengeMethod, OAuth2Client } from "google-auth-library";
5
+ /**
6
+ * Google login parameters
7
+ * https://developers.google.com/identity/protocols/oauth2/openid-connect
8
+ */
9
+ export class GoogleParameters extends OAuthProviderParameters {
10
+ /**
11
+ * @param params - raw parameters
12
+ * @returns this
13
+ */
14
+ load(params = {}) {
15
+ super.load(params);
16
+ this.access_type ?? (this.access_type = "online");
17
+ this.scope ?? (this.scope = ["openid", "email", "profile"]);
18
+ this.audiences ?? (this.audiences = []);
19
+ return this;
20
+ }
21
+ }
22
+ /**
23
+ * Sign in with Google (OpenID Connect)
24
+ *
25
+ * The browser flow (`GET /auth/google`, `GET /auth/google/callback`) uses PKCE and a nonce, exchanges the code and
26
+ * verifies the returned ID token for `client_id`. `Auth.Google.Token` accepts a Google ID token, as `token` or as
27
+ * the `id_token` of `tokens` (v3 body), whose audience is `client_id` or one of `audiences` (never an access
28
+ * token). Credentials are stored encrypted on the ident and emitted with `GoogleAuth.Tokens` after the login.
29
+ * @WebdaModda GoogleAuthentication
30
+ */
31
+ export class GoogleAuthentication extends OAuthProvider {
32
+ constructor() {
33
+ super(...arguments);
34
+ this.providerName = "google";
35
+ }
36
+ /** @override */
37
+ async init() {
38
+ await super.init();
39
+ this.verifier = new OAuth2Client({ clientId: this.parameters.client_id });
40
+ return this;
41
+ }
42
+ /**
43
+ * @param redirectUri - callback url
44
+ * @returns a Google OAuth client for the authorization url and the code exchange
45
+ */
46
+ getClient(redirectUri) {
47
+ return new OAuth2Client({
48
+ clientId: this.parameters.client_id,
49
+ clientSecret: this.parameters.client_secret,
50
+ redirectUri
51
+ });
52
+ }
53
+ /**
54
+ * @param request - authorization request
55
+ * @returns the Google authorization url
56
+ */
57
+ getAuthorizationUrl(request) {
58
+ const { auth_options, access_type, hostedDomain } = this.parameters;
59
+ return this.getClient(request.redirectUri).generateAuthUrl({
60
+ ...(auth_options ?? {}),
61
+ access_type,
62
+ scope: request.scope,
63
+ state: request.state,
64
+ redirect_uri: request.redirectUri,
65
+ response_type: "code",
66
+ code_challenge_method: CodeChallengeMethod.S256,
67
+ code_challenge: request.codeChallenge,
68
+ nonce: request.nonce,
69
+ ...(hostedDomain ? { hd: hostedDomain } : {})
70
+ });
71
+ }
72
+ /**
73
+ * Exchange the code with the PKCE verifier, verify the returned ID token for `client_id` and its nonce
74
+ * @param request - code exchange
75
+ * @returns the identity, with the credentials of the exchange
76
+ */
77
+ async handleCallback(request) {
78
+ let tokens;
79
+ try {
80
+ ({ tokens } = await this.getClient(request.redirectUri).getToken({
81
+ code: request.code,
82
+ codeVerifier: request.codeVerifier,
83
+ redirect_uri: request.redirectUri
84
+ }));
85
+ }
86
+ catch (err) {
87
+ this.log("WARN", "Google code exchange failed:", safeErrorReason(err));
88
+ throw new TokenInvalid("Code exchange failed");
89
+ }
90
+ if (!tokens?.id_token) {
91
+ throw new TokenInvalid("Google returned no ID token: the 'openid' scope is required");
92
+ }
93
+ const payload = await this.verifyIdToken(tokens.id_token, [this.parameters.client_id]);
94
+ if (typeof payload.nonce !== "string" || !sameSecret(payload.nonce, request.nonce)) {
95
+ throw new TokenInvalid("ID token nonce mismatch");
96
+ }
97
+ return this.toIdentity(payload, tokens);
98
+ }
99
+ /**
100
+ * Verify a Google ID token sent by a client, as `token` or `tokens.id_token`
101
+ * @param request - token request
102
+ * @returns the identity, with the credentials sent in `tokens`
103
+ */
104
+ async handleToken(request) {
105
+ const fromTokens = request.tokens?.id_token;
106
+ if (request.token && fromTokens !== undefined && fromTokens !== request.token) {
107
+ throw new WebdaError.BadRequest("token and tokens.id_token differ");
108
+ }
109
+ const idToken = request.token ?? fromTokens;
110
+ if (typeof idToken !== "string" || !idToken) {
111
+ throw new TokenInvalid("A Google ID token is required");
112
+ }
113
+ const payload = await this.verifyIdToken(idToken, [
114
+ this.parameters.client_id,
115
+ ...(this.parameters.audiences ?? [])
116
+ ]);
117
+ return this.toIdentity(payload, request.tokens);
118
+ }
119
+ /**
120
+ * Emit `GoogleAuth.Tokens` once the login succeeded
121
+ * @param identity - the identity
122
+ * @param _result - the result
123
+ * @param source - browser callback or token operation
124
+ * @param request - the token operation request
125
+ */
126
+ async onAuthenticated(identity, _result, source, request) {
127
+ const tokens = source === "token" ? (request?.tokens ?? { id_token: request?.token }) : identity.tokens;
128
+ await this.emit("GoogleAuth.Tokens", { tokens, context: useContext() });
129
+ }
130
+ /**
131
+ * Verify an ID token: signature, expiry, issuer (google-auth-library), audience and hosted domain
132
+ * @param idToken - ID token
133
+ * @param audiences - accepted audiences
134
+ * @returns the payload
135
+ * @throws TokenInvalid when the token does not verify
136
+ * @throws EmailDomainNotAllowed when `hostedDomain` is set and the token is not from that domain
137
+ */
138
+ async verifyIdToken(idToken, audiences) {
139
+ const audience = audiences.filter(a => typeof a === "string" && a);
140
+ // Without audience the library would skip the check: never verify without one
141
+ if (!audience.length) {
142
+ throw new TokenInvalid("No audience configured");
143
+ }
144
+ this.verifier ?? (this.verifier = new OAuth2Client({ clientId: this.parameters.client_id }));
145
+ let payload;
146
+ try {
147
+ const ticket = await this.verifier.verifyIdToken({
148
+ idToken,
149
+ audience: audience.length === 1 ? audience[0] : audience
150
+ });
151
+ payload = ticket.getPayload();
152
+ }
153
+ catch (err) {
154
+ // The library messages contain the token or its payload: only the error class is logged
155
+ this.log("WARN", "Google ID token verification failed:", safeErrorReason(err));
156
+ throw new TokenInvalid();
157
+ }
158
+ if (!payload?.sub) {
159
+ throw new TokenInvalid("ID token has no subject");
160
+ }
161
+ const hostedDomain = this.parameters.hostedDomain?.trim().toLowerCase();
162
+ if (hostedDomain && payload.hd?.toLowerCase() !== hostedDomain) {
163
+ throw new EmailDomainNotAllowed("Google account is not from the hosted domain");
164
+ }
165
+ return payload;
166
+ }
167
+ /**
168
+ * @param payload - verified ID token payload
169
+ * @param tokens - credentials to store
170
+ * @returns the identity
171
+ */
172
+ toIdentity(payload, tokens) {
173
+ return {
174
+ provider: this.providerName,
175
+ providerUid: payload.sub,
176
+ email: payload.email,
177
+ emailVerified: payload.email_verified === true,
178
+ profile: { name: payload.name, picture: payload.picture, locale: payload.locale, hd: payload.hd },
179
+ tokens,
180
+ amr: ["oauth"]
181
+ };
182
+ }
183
+ }
184
+ GoogleAuthentication.Parameters = GoogleParameters;
185
+ /**
186
+ * Constant-time comparison
187
+ * @param a - received value
188
+ * @param b - expected value
189
+ * @returns true when equal
190
+ */
191
+ function sameSecret(a, b) {
192
+ const left = Buffer.from(a);
193
+ const right = Buffer.from(b);
194
+ return left.length === right.length && timingSafeEqual(left, right);
195
+ }
196
+ export default GoogleAuthentication;
197
+ //# sourceMappingURL=google-auth.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-auth.service.js","sourceRoot":"","sources":["../src/google-auth.service.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./google-auth.js";
1
+ export * from "./google-auth.service.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}
package/lib/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./google-auth.js";
1
+ export * from "./google-auth.service.js";
2
2
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,18 +1,8 @@
1
1
  {
2
2
  "name": "@webda/google-auth",
3
- "version": "4.0.0-beta.2",
3
+ "version": "4.0.0-beta.4",
4
4
  "description": "Webda Google Authentication",
5
5
  "main": "lib/index.js",
6
- "scripts": {
7
- "build": "webda build",
8
- "build:watch": "webda build --watch",
9
- "pretest": "npm run build",
10
- "lint": "eslint",
11
- "lint:fix": "eslint --fix",
12
- "test": "c8 mocha",
13
- "format": "prettier --check src/**/*",
14
- "format:fix": "prettier --write src/**/*"
15
- },
16
6
  "repository": {
17
7
  "type": "git",
18
8
  "url": "git+https://github.com/loopingz/webda.io.git"
@@ -33,41 +23,47 @@
33
23
  },
34
24
  "homepage": "https://github.com/loopingz/webda.io#readme",
35
25
  "dependencies": {
36
- "@webda/core": "^4.0.0-beta.2",
37
- "google-auth-library": "^9.0.0"
38
- },
39
- "c8": {
40
- "report-dir": "./reports",
41
- "reporter": [
42
- "html",
43
- "lcov",
44
- "json",
45
- "text"
46
- ],
47
- "exclude": [
48
- "**/*.spec.ts"
49
- ]
26
+ "google-auth-library": "^10.6.2",
27
+ "@webda/auth": "4.0.0-beta.4",
28
+ "@webda/core": "4.0.0-beta.4"
50
29
  },
51
30
  "publishConfig": {
52
31
  "access": "public"
53
32
  },
54
33
  "devDependencies": {
55
- "@testdeck/mocha": "^0.3.2",
56
- "@types/node": "20.0.0",
57
- "@webda/shell": "^4.0.0-beta.2",
58
- "node-fetch": "^3.3.1",
59
- "sinon": "^19.0.2",
60
- "tsx": "^4.17.0"
34
+ "@types/node": "25.5.0",
35
+ "@vitest/coverage-v8": "^4.1.2",
36
+ "jscodeshift": "^17.3.0",
37
+ "skott": "^0.35.8",
38
+ "tsx": "^4.21.0",
39
+ "typescript": "7.1.0-dev.20260929.1",
40
+ "vite": "^6.0.0",
41
+ "vitest": "^4.1.2",
42
+ "@webda/compiler": "^4.0.0-beta.4",
43
+ "@webda/content-mapper": "^4.0.0-beta.4",
44
+ "@webda/test": "4.0.0-beta.3",
45
+ "@webda/workout": "4.0.0-beta.3"
61
46
  },
62
47
  "type": "module",
63
48
  "engines": {
64
- "node": ">=20.0.0"
49
+ "node": ">=22.0.0"
65
50
  },
66
51
  "nx": {
67
52
  "implicitDependencies": [
68
53
  "@webda/core",
69
- "@webda/shell"
54
+ "@webda/auth"
70
55
  ]
71
56
  },
72
- "gitHead": "e992dcfb82cefa6cd5c61c54a0e63778b6f53d8e"
73
- }
57
+ "scripts": {
58
+ "build": "webdac build",
59
+ "build:watch": "webdac build --watch",
60
+ "pretest": "npm run build",
61
+ "lint": "eslint",
62
+ "lint:fix": "eslint --fix",
63
+ "test": "vitest run",
64
+ "format": "prettier --check src/**/*",
65
+ "format:fix": "prettier --write src/**/*",
66
+ "deps:graph": "skott --showCircularDependencies --no-trackTypeOnlyDependencies --displayMode=raw -e .ts",
67
+ "codemod": "jscodeshift --parser=ts --extensions=tsx,ts -t ../codemod/current.mjs src"
68
+ }
69
+ }