@webda/google-auth 4.0.0-beta.3 → 4.0.0-beta.5
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 +59 -56
- package/lib/google-auth.service.d.ts +84 -95
- package/lib/google-auth.service.d.ts.map +1 -1
- package/lib/google-auth.service.js +151 -195
- package/lib/google-auth.service.js.map +1 -1
- package/package.json +11 -6
- package/webda.module.json +90 -60
package/README.md
CHANGED
|
@@ -15,91 +15,94 @@ This module is part of Webda Application Framework that allows you to quickly de
|
|
|
15
15
|
|
|
16
16
|
# @webda/google-auth
|
|
17
17
|
|
|
18
|
-
> Google
|
|
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).
|
|
19
21
|
|
|
20
22
|
## When to use it
|
|
21
23
|
|
|
22
|
-
- You want to
|
|
23
|
-
|
|
24
|
-
- You
|
|
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).
|
|
25
28
|
|
|
26
29
|
## Install
|
|
27
30
|
|
|
28
31
|
```bash
|
|
29
|
-
pnpm add @webda/google-auth
|
|
32
|
+
pnpm add @webda/auth @webda/google-auth
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
## Configuration
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
`GoogleAuthentication` is a provider service next to `Authentication` (see the
|
|
38
|
+
[Authentication guide](https://docs.webda.io/Security/Authentication)):
|
|
39
|
+
|
|
40
|
+
```jsonc
|
|
35
41
|
{
|
|
36
42
|
"services": {
|
|
37
|
-
"Authentication": {
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
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
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
```
|
|
52
62
|
|
|
53
|
-
| Parameter
|
|
54
|
-
|
|
55
|
-
| `client_id`
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
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` |
|
|
62
76
|
|
|
63
77
|
## Usage
|
|
64
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
|
+
|
|
65
93
|
```typescript
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// 2. Configure (see above). The framework auto-registers these routes:
|
|
71
|
-
// GET /auth/google → redirects to Google consent screen
|
|
72
|
-
// GET /auth/google/callback → handles the OAuth callback, creates a session
|
|
73
|
-
|
|
74
|
-
// 3. Listen to the auth event to customize post-login behaviour:
|
|
75
|
-
import { GoogleAuthentication } from "@webda/google-auth";
|
|
76
|
-
import { Bean, Inject } from "@webda/core";
|
|
77
|
-
|
|
78
|
-
@Bean
|
|
79
|
-
export class ProfileSync extends Service {
|
|
80
|
-
@Inject("Authentication")
|
|
81
|
-
auth: any;
|
|
82
|
-
|
|
83
|
-
async resolve(): Promise<this> {
|
|
84
|
-
await super.resolve();
|
|
85
|
-
// Fired after a successful Google login
|
|
86
|
-
this.auth.on("GoogleAuth.Tokens", async ({ tokens, context }) => {
|
|
87
|
-
// tokens.access_token, tokens.refresh_token (if access_type=offline)
|
|
88
|
-
const user = context.getCurrentUser();
|
|
89
|
-
// ... sync Google profile data to your User model
|
|
90
|
-
});
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
+
useService("google").on("GoogleAuth.Tokens", async ({ tokens, context }) => {
|
|
95
|
+
// tokens.access_token, tokens.refresh_token (access_type "offline")
|
|
96
|
+
});
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
## Reference
|
|
97
100
|
|
|
98
|
-
- API reference: see the auto-generated typedoc at `docs/pages/Modules/google-auth/`.
|
|
99
101
|
- Source: [`packages/google-auth`](https://github.com/loopingz/webda.io/tree/main/packages/google-auth)
|
|
100
|
-
- Related: [`@webda/
|
|
102
|
+
- Related: [`@webda/auth`](../auth) (`OAuthProvider` base class, `Authentication` service).
|
|
101
103
|
|
|
102
104
|
<!-- README_FOOTER -->
|
|
105
|
+
|
|
103
106
|
## Sponsors
|
|
104
107
|
|
|
105
108
|
<!--
|
|
@@ -1,136 +1,125 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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 {
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
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
|
|
6
10
|
*/
|
|
7
11
|
tokens: Credentials;
|
|
8
12
|
}
|
|
13
|
+
/** Events of GoogleAuthentication */
|
|
14
|
+
export type GoogleAuthEvents = {
|
|
15
|
+
"GoogleAuth.Tokens": EventGoogleOAuthToken;
|
|
16
|
+
};
|
|
9
17
|
/**
|
|
10
|
-
*
|
|
11
|
-
* https://developers.google.com/identity/protocols/oauth2
|
|
18
|
+
* Google login parameters
|
|
19
|
+
* https://developers.google.com/identity/protocols/oauth2/openid-connect
|
|
12
20
|
*/
|
|
13
|
-
export declare class GoogleParameters extends
|
|
21
|
+
export declare class GoogleParameters extends OAuthProviderParameters {
|
|
14
22
|
/**
|
|
15
|
-
* Google
|
|
23
|
+
* Google OAuth client id (web application)
|
|
16
24
|
*/
|
|
17
25
|
client_id: string;
|
|
18
26
|
/**
|
|
19
|
-
* Google
|
|
27
|
+
* Google OAuth client secret
|
|
20
28
|
*/
|
|
21
29
|
client_secret: string;
|
|
22
30
|
/**
|
|
23
|
-
*
|
|
31
|
+
* Scopes requested from Google
|
|
32
|
+
* @default ["openid", "email", "profile"]
|
|
24
33
|
*/
|
|
25
|
-
|
|
34
|
+
scope?: string[];
|
|
26
35
|
/**
|
|
27
|
-
* Type of access
|
|
28
|
-
*
|
|
29
|
-
* online by default
|
|
36
|
+
* Type of access: "offline" also returns a refresh token (stored encrypted in the ident tokens)
|
|
37
|
+
* @default "online"
|
|
30
38
|
*/
|
|
31
39
|
access_type?: "online" | "offline";
|
|
32
|
-
auth_options?: any;
|
|
33
|
-
redirects?: {
|
|
34
|
-
use_referer: boolean;
|
|
35
|
-
whitelist: string[];
|
|
36
|
-
defaults: {
|
|
37
|
-
[key: string]: string;
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Load parameters with Google defaults
|
|
42
|
-
* @param params - the service parameters
|
|
43
|
-
* @returns this
|
|
44
|
-
*/
|
|
45
|
-
load(params?: any): this;
|
|
46
|
-
}
|
|
47
|
-
type GoogleAuthEvents = OAuthEvents & {
|
|
48
|
-
"GoogleAuth.Tokens": EventGoogleOAuthToken;
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Manage Google Authentication
|
|
52
|
-
*
|
|
53
|
-
* @WebdaModda
|
|
54
|
-
*/
|
|
55
|
-
export default class GoogleAuthentication<T extends GoogleParameters = GoogleParameters> extends OAuthService<T, GoogleAuthEvents> implements RequestFilter<WebContext> {
|
|
56
|
-
protected _client: OAuth2Client;
|
|
57
40
|
/**
|
|
58
|
-
*
|
|
59
|
-
* @
|
|
41
|
+
* Other client ids whose ID tokens `Auth.Google.Token` accepts (for example Android/iOS client ids)
|
|
42
|
+
* @default []
|
|
60
43
|
*/
|
|
61
|
-
|
|
44
|
+
audiences?: string[];
|
|
62
45
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
65
48
|
*/
|
|
66
|
-
|
|
49
|
+
hostedDomain?: string;
|
|
67
50
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
70
54
|
*/
|
|
71
|
-
|
|
72
|
-
name: string;
|
|
73
|
-
required: boolean;
|
|
74
|
-
}[];
|
|
55
|
+
auth_options?: Record<string, any>;
|
|
75
56
|
/**
|
|
76
|
-
*
|
|
77
|
-
* @returns
|
|
57
|
+
* @param params - raw parameters
|
|
58
|
+
* @returns this
|
|
78
59
|
*/
|
|
79
|
-
|
|
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>;
|
|
80
78
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @returns
|
|
79
|
+
* @param redirectUri - callback url
|
|
80
|
+
* @returns a Google OAuth client for the authorization url and the code exchange
|
|
83
81
|
*/
|
|
84
|
-
|
|
82
|
+
protected getClient(redirectUri?: string): OAuth2Client;
|
|
85
83
|
/**
|
|
86
|
-
*
|
|
87
|
-
* @
|
|
88
|
-
* @param state - the random state to verify on callback
|
|
89
|
-
* @param _ctx - the request context
|
|
90
|
-
* @returns the authorization url
|
|
84
|
+
* @param request - authorization request
|
|
85
|
+
* @returns the Google authorization url
|
|
91
86
|
*/
|
|
92
|
-
|
|
87
|
+
getAuthorizationUrl(request: OAuthAuthorizationRequest): string;
|
|
93
88
|
/**
|
|
94
|
-
*
|
|
95
|
-
* @param
|
|
96
|
-
* @returns
|
|
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
|
|
97
92
|
*/
|
|
98
|
-
|
|
93
|
+
handleCallback(request: OAuthCallbackRequest): Promise<ResolvedIdentity>;
|
|
99
94
|
/**
|
|
100
|
-
*
|
|
101
|
-
* @param
|
|
102
|
-
* @returns the identity
|
|
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`
|
|
103
98
|
*/
|
|
104
|
-
|
|
105
|
-
identId: any;
|
|
106
|
-
profile: any;
|
|
107
|
-
}>;
|
|
99
|
+
handleToken(request: OAuthTokenRequest): Promise<ResolvedIdentity>;
|
|
108
100
|
/**
|
|
109
|
-
*
|
|
110
|
-
* @param
|
|
111
|
-
* @
|
|
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
|
|
112
106
|
*/
|
|
113
|
-
|
|
107
|
+
protected onAuthenticated(identity: ResolvedIdentity, _result: AuthResult, source: "callback" | "token", request?: OAuthTokenRequest): Promise<void>;
|
|
114
108
|
/**
|
|
115
|
-
* Verify
|
|
116
|
-
* @param
|
|
117
|
-
* @
|
|
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
|
|
118
115
|
*/
|
|
119
|
-
|
|
120
|
-
identId: string;
|
|
121
|
-
profile: import("google-auth-library").TokenPayload;
|
|
122
|
-
}>;
|
|
116
|
+
protected verifyIdToken(idToken: string, audiences: string[]): Promise<TokenPayload>;
|
|
123
117
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* Store the token in user store afterwards
|
|
128
|
-
* @param token - existing credentials to reuse, if any
|
|
129
|
-
* @param open - callback to open the authorization url in a browser
|
|
130
|
-
* @param storeToken - callback to persist the retrieved credentials
|
|
131
|
-
* @returns the authenticated OAuth2 client
|
|
118
|
+
* @param payload - verified ID token payload
|
|
119
|
+
* @param tokens - credentials to store
|
|
120
|
+
* @returns the identity
|
|
132
121
|
*/
|
|
133
|
-
|
|
122
|
+
protected toIdentity(payload: TokenPayload, tokens?: Credentials): ResolvedIdentity;
|
|
134
123
|
}
|
|
135
|
-
export
|
|
124
|
+
export default GoogleAuthentication;
|
|
136
125
|
//# sourceMappingURL=google-auth.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-auth.service.d.ts","sourceRoot":"","sources":["../src/google-auth.service.ts"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -1,241 +1,197 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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";
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
* https://developers.google.com/identity/protocols/oauth2
|
|
6
|
+
* Google login parameters
|
|
7
|
+
* https://developers.google.com/identity/protocols/oauth2/openid-connect
|
|
7
8
|
*/
|
|
8
|
-
export class GoogleParameters extends
|
|
9
|
+
export class GoogleParameters extends OAuthProviderParameters {
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @param params - the service parameters
|
|
11
|
+
* @param params - raw parameters
|
|
12
12
|
* @returns this
|
|
13
13
|
*/
|
|
14
14
|
load(params = {}) {
|
|
15
15
|
super.load(params);
|
|
16
16
|
this.access_type ?? (this.access_type = "online");
|
|
17
|
+
this.scope ?? (this.scope = ["openid", "email", "profile"]);
|
|
18
|
+
this.audiences ?? (this.audiences = []);
|
|
17
19
|
return this;
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
23
|
+
* Sign in with Google (OpenID Connect)
|
|
22
24
|
*
|
|
23
|
-
*
|
|
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
|
|
24
30
|
*/
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*/
|
|
30
|
-
getName() {
|
|
31
|
-
return "google";
|
|
31
|
+
export class GoogleAuthentication extends OAuthProvider {
|
|
32
|
+
constructor() {
|
|
33
|
+
super(...arguments);
|
|
34
|
+
this.providerName = "google";
|
|
32
35
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return [/accounts\.google\.[a-z]+$/];
|
|
36
|
+
/** @override */
|
|
37
|
+
async init() {
|
|
38
|
+
await super.init();
|
|
39
|
+
this.verifier = new OAuth2Client({ clientId: this.parameters.client_id });
|
|
40
|
+
return this;
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
|
-
*
|
|
42
|
-
* @returns the
|
|
43
|
+
* @param redirectUri - callback url
|
|
44
|
+
* @returns a Google OAuth client for the authorization url and the code exchange
|
|
43
45
|
*/
|
|
44
|
-
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
name: "scope",
|
|
52
|
-
required: true
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: "state",
|
|
56
|
-
required: true
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "authuser",
|
|
60
|
-
required: false
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "hd",
|
|
64
|
-
required: false
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: "prompt",
|
|
68
|
-
required: false
|
|
69
|
-
}
|
|
70
|
-
];
|
|
46
|
+
getClient(redirectUri) {
|
|
47
|
+
return new OAuth2Client({
|
|
48
|
+
clientId: this.parameters.client_id,
|
|
49
|
+
clientSecret: this.parameters.client_secret,
|
|
50
|
+
redirectUri
|
|
51
|
+
});
|
|
71
52
|
}
|
|
72
53
|
/**
|
|
73
|
-
*
|
|
74
|
-
* @returns the
|
|
54
|
+
* @param request - authorization request
|
|
55
|
+
* @returns the Google authorization url
|
|
75
56
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
});
|
|
78
71
|
}
|
|
79
72
|
/**
|
|
80
|
-
*
|
|
81
|
-
* @
|
|
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
|
|
82
76
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
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);
|
|
85
98
|
}
|
|
86
99
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @param
|
|
89
|
-
* @
|
|
90
|
-
* @param _ctx - the request context
|
|
91
|
-
* @returns the authorization url
|
|
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`
|
|
92
103
|
*/
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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);
|
|
103
118
|
}
|
|
104
119
|
/**
|
|
105
|
-
*
|
|
106
|
-
* @param
|
|
107
|
-
* @
|
|
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
|
|
108
125
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
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() });
|
|
111
129
|
}
|
|
112
130
|
/**
|
|
113
|
-
*
|
|
114
|
-
* @param
|
|
115
|
-
* @
|
|
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
|
|
116
137
|
*/
|
|
117
|
-
async
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
throw new
|
|
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");
|
|
122
143
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const oauthClient = this.getOAuthClient(redirect_uri);
|
|
126
|
-
let profile, identId;
|
|
127
|
-
// Now that we have the code, use that to acquire tokens.
|
|
144
|
+
this.verifier ?? (this.verifier = new OAuth2Client({ clientId: this.parameters.client_id }));
|
|
145
|
+
let payload;
|
|
128
146
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
const ticket = await this.verifier.verifyIdToken({
|
|
148
|
+
idToken,
|
|
149
|
+
audience: audience.length === 1 ? audience[0] : audience
|
|
150
|
+
});
|
|
151
|
+
payload = ticket.getPayload();
|
|
133
152
|
}
|
|
134
153
|
catch (err) {
|
|
135
|
-
|
|
136
|
-
|
|
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();
|
|
137
157
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* @returns the token payload
|
|
147
|
-
*/
|
|
148
|
-
async getUserInfo(token) {
|
|
149
|
-
const oauthClient = this.getOAuthClient();
|
|
150
|
-
const ticket = await oauthClient.verifyIdToken({
|
|
151
|
-
idToken: token,
|
|
152
|
-
audience: this.parameters.client_id
|
|
153
|
-
});
|
|
154
|
-
return ticket.getPayload();
|
|
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;
|
|
155
166
|
}
|
|
156
167
|
/**
|
|
157
|
-
*
|
|
158
|
-
* @param
|
|
159
|
-
* @returns the identity
|
|
168
|
+
* @param payload - verified ID token payload
|
|
169
|
+
* @param tokens - credentials to store
|
|
170
|
+
* @returns the identity
|
|
160
171
|
*/
|
|
161
|
-
|
|
162
|
-
const tokens = (await context.getRequestBody()).tokens;
|
|
163
|
-
if (!tokens) {
|
|
164
|
-
throw new WebdaError.BadRequest("No tokens provided");
|
|
165
|
-
}
|
|
166
|
-
const profile = await this.getUserInfo(tokens.id_token);
|
|
172
|
+
toIdentity(payload, tokens) {
|
|
167
173
|
return {
|
|
168
|
-
|
|
169
|
-
|
|
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"]
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Retrieve Google Client
|
|
174
|
-
*
|
|
175
|
-
* Redirecting to the webbrowser for the OAuth validation
|
|
176
|
-
* Store the token in user store afterwards
|
|
177
|
-
* @param token - existing credentials to reuse, if any
|
|
178
|
-
* @param open - callback to open the authorization url in a browser
|
|
179
|
-
* @param storeToken - callback to persist the retrieved credentials
|
|
180
|
-
* @returns the authenticated OAuth2 client
|
|
181
|
-
*/
|
|
182
|
-
async getLocalClient(token, open, storeToken) {
|
|
183
|
-
if (this._client) {
|
|
184
|
-
return this._client;
|
|
185
|
-
}
|
|
186
|
-
const oAuth2Client = this.getOAuthClient();
|
|
187
|
-
if (token) {
|
|
188
|
-
oAuth2Client.setCredentials(token);
|
|
189
|
-
this._client = oAuth2Client;
|
|
190
|
-
return oAuth2Client;
|
|
191
|
-
}
|
|
192
|
-
// Generate the url that will be used for the consent dialog.
|
|
193
|
-
const authorizeUrl = oAuth2Client.generateAuthUrl({
|
|
194
|
-
access_type: "offline",
|
|
195
|
-
scope: this.parameters.scope,
|
|
196
|
-
redirect_uri: "http://localhost:3000/oauth2callback"
|
|
197
|
-
});
|
|
198
|
-
// Open an http server to accept the oauth callback. In this simple example, the
|
|
199
|
-
// only request to our webserver is to /oauth2callback?code=<code>
|
|
200
|
-
return new Promise((resolve, reject) => {
|
|
201
|
-
const server = http
|
|
202
|
-
.createServer(async (req, res) => {
|
|
203
|
-
if (req.url.indexOf("/oauth2callback") > -1) {
|
|
204
|
-
try {
|
|
205
|
-
// acquire the code from the querystring, and close the web server.
|
|
206
|
-
const code = new URL(req.url, `http://localhost:3000`).searchParams.get("code");
|
|
207
|
-
if (code) {
|
|
208
|
-
res.end("Authentication successful! Please return to the console.");
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
res.end("Authentication unsuccessful! Please return to the console.");
|
|
212
|
-
return reject("Failed");
|
|
213
|
-
}
|
|
214
|
-
// Now that we have the code, use that to acquire tokens.
|
|
215
|
-
const r = await oAuth2Client.getToken(code);
|
|
216
|
-
// Make sure to set the credentials on the OAuth2 client.
|
|
217
|
-
storeToken(r.tokens);
|
|
218
|
-
oAuth2Client.setCredentials(r.tokens);
|
|
219
|
-
this._client = oAuth2Client;
|
|
220
|
-
this.log("INFO", "Google Authentication finished.");
|
|
221
|
-
return resolve(this._client);
|
|
222
|
-
}
|
|
223
|
-
catch (err) {
|
|
224
|
-
this.log("ERROR", err);
|
|
225
|
-
reject(err);
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
228
|
-
server.close();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
.listen(3000, () => {
|
|
233
|
-
// open the browser to the authorize url to start the workflow
|
|
234
|
-
this.log("INFO", "Launching your browser for Google API Permission");
|
|
235
|
-
open(authorizeUrl);
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
183
|
}
|
|
240
|
-
|
|
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;
|
|
241
197
|
//# sourceMappingURL=google-auth.service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-auth.service.js","sourceRoot":"","sources":["../src/google-auth.service.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"google-auth.service.js","sourceRoot":"","sources":["../src/google-auth.service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webda/google-auth",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.5",
|
|
4
4
|
"description": "Webda Google Authentication",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/loopingz/webda.io.git"
|
|
8
|
+
"url": "git+https://github.com/loopingz/webda.io.git",
|
|
9
|
+
"directory": "packages/google-auth"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
11
12
|
"webda",
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
"homepage": "https://github.com/loopingz/webda.io#readme",
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"google-auth-library": "^10.6.2",
|
|
27
|
-
"@webda/
|
|
28
|
+
"@webda/auth": "4.0.0-beta.5",
|
|
29
|
+
"@webda/core": "4.0.0-beta.5"
|
|
28
30
|
},
|
|
29
31
|
"publishConfig": {
|
|
30
32
|
"access": "public"
|
|
@@ -38,8 +40,10 @@
|
|
|
38
40
|
"typescript": "7.1.0-dev.20260929.1",
|
|
39
41
|
"vite": "^6.0.0",
|
|
40
42
|
"vitest": "^4.1.2",
|
|
41
|
-
"@webda/compiler": "^4.0.0-beta.
|
|
42
|
-
"@webda/
|
|
43
|
+
"@webda/compiler": "^4.0.0-beta.5",
|
|
44
|
+
"@webda/content-mapper": "^4.0.0-beta.5",
|
|
45
|
+
"@webda/workout": "4.0.0-beta.5",
|
|
46
|
+
"@webda/test": "4.0.0-beta.5"
|
|
43
47
|
},
|
|
44
48
|
"type": "module",
|
|
45
49
|
"engines": {
|
|
@@ -47,7 +51,8 @@
|
|
|
47
51
|
},
|
|
48
52
|
"nx": {
|
|
49
53
|
"implicitDependencies": [
|
|
50
|
-
"@webda/core"
|
|
54
|
+
"@webda/core",
|
|
55
|
+
"@webda/auth"
|
|
51
56
|
]
|
|
52
57
|
},
|
|
53
58
|
"scripts": {
|
package/webda.module.json
CHANGED
|
@@ -4,112 +4,125 @@
|
|
|
4
4
|
"deployers": {},
|
|
5
5
|
"moddas": {
|
|
6
6
|
"Webda/GoogleAuthentication": {
|
|
7
|
-
"Import": "lib/google-auth.service:
|
|
7
|
+
"Import": "lib/google-auth.service:GoogleAuthentication",
|
|
8
8
|
"Schema": {
|
|
9
9
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
10
10
|
"additionalProperties": false,
|
|
11
11
|
"definitions": {
|
|
12
|
-
"
|
|
12
|
+
"OAuthRedirects": {
|
|
13
13
|
"additionalProperties": false,
|
|
14
|
+
"description": "Browser redirects of the OAuth callback",
|
|
14
15
|
"properties": {
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"use_referer": {
|
|
19
|
-
"default": false,
|
|
20
|
-
"type": "boolean"
|
|
16
|
+
"failure": {
|
|
17
|
+
"description": "Where a failed login or callback lands, with `?reason=<CODE>` (required)",
|
|
18
|
+
"type": "string"
|
|
21
19
|
},
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"type": "array"
|
|
20
|
+
"success": {
|
|
21
|
+
"default": "/",
|
|
22
|
+
"description": "Where a successful login lands when the login did not carry an allowed `redirect` parameter",
|
|
23
|
+
"type": "string"
|
|
27
24
|
}
|
|
28
25
|
},
|
|
29
|
-
"required": [
|
|
30
|
-
"defaults",
|
|
31
|
-
"whitelist"
|
|
32
|
-
],
|
|
33
26
|
"type": "object"
|
|
34
27
|
},
|
|
35
|
-
"
|
|
36
|
-
"additionalProperties": {
|
|
37
|
-
|
|
38
|
-
},
|
|
28
|
+
"Record<string, any>": {
|
|
29
|
+
"additionalProperties": {},
|
|
30
|
+
"description": "Construct a type with a set of properties K of type T",
|
|
39
31
|
"type": "object"
|
|
40
32
|
}
|
|
41
33
|
},
|
|
42
|
-
"description": "
|
|
34
|
+
"description": "Google login parameters\nhttps://developers.google.com/identity/protocols/oauth2/openid-connect",
|
|
43
35
|
"properties": {
|
|
44
36
|
"access_type": {
|
|
45
|
-
"
|
|
37
|
+
"default": "online",
|
|
38
|
+
"description": "Type of access: \"offline\" also returns a refresh token (stored encrypted in the ident tokens)",
|
|
46
39
|
"enum": [
|
|
47
40
|
"online",
|
|
48
41
|
"offline"
|
|
49
42
|
],
|
|
50
43
|
"type": "string"
|
|
51
44
|
},
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
"allowedEmailDomains": {
|
|
46
|
+
"description": "Only these email domains may use this provider (see ProviderEmailPolicy)",
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "string"
|
|
49
|
+
},
|
|
50
|
+
"type": "array"
|
|
51
|
+
},
|
|
52
|
+
"audiences": {
|
|
53
|
+
"default": [],
|
|
54
|
+
"description": "Other client ids whose ID tokens `Auth.Google.Token` accepts (for example Android/iOS client ids)",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"type": "array"
|
|
59
|
+
},
|
|
60
|
+
"auth_options": {
|
|
61
|
+
"$ref": "#/definitions/Record%3Cstring%2C%20any%3E",
|
|
62
|
+
"description": "Additional authorization url parameters (for example `prompt`, `login_hint`); cannot override `state`,\n`redirect_uri`, `scope`, `access_type`, `response_type`, `code_challenge*`, `nonce` nor `hd`\nSee https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters"
|
|
57
63
|
},
|
|
58
64
|
"authorized_uris": {
|
|
59
65
|
"default": [],
|
|
60
|
-
"description": "
|
|
66
|
+
"description": "Allowed post-login targets for the `redirect` query parameter of the login route: absolute http(s) urls, a\ntarget matches when it has the same origin and its path is the listed path or below it",
|
|
61
67
|
"items": {
|
|
62
68
|
"type": "string"
|
|
63
69
|
},
|
|
64
70
|
"type": "array"
|
|
65
71
|
},
|
|
66
72
|
"client_id": {
|
|
67
|
-
"description": "Google
|
|
73
|
+
"description": "Google OAuth client id (web application)",
|
|
68
74
|
"type": "string"
|
|
69
75
|
},
|
|
70
76
|
"client_secret": {
|
|
71
|
-
"description": "Google
|
|
77
|
+
"description": "Google OAuth client secret",
|
|
72
78
|
"type": "string"
|
|
73
79
|
},
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"description": "If set to true it will add a ${url}/scope\nSo client can anticipate the requested scope\n\nThis is useful when using the client api to generate\ntoken. This way the service can request the wanted scope",
|
|
77
|
-
"type": "boolean"
|
|
78
|
-
},
|
|
79
|
-
"no_referer": {
|
|
80
|
-
"default": false,
|
|
81
|
-
"description": "Allow direct connection without a referer",
|
|
82
|
-
"type": "boolean"
|
|
83
|
-
},
|
|
84
|
-
"project_id": {
|
|
85
|
-
"description": "Google Project ID",
|
|
80
|
+
"hostedDomain": {
|
|
81
|
+
"description": "Only accept Google Workspace accounts of this domain (the `hd` claim of the ID token); also sent as a hint to\nthe account chooser",
|
|
86
82
|
"type": "string"
|
|
87
83
|
},
|
|
88
84
|
"redirect_uri": {
|
|
89
|
-
"default": "
|
|
90
|
-
"description": "
|
|
85
|
+
"default": "\"<absolute url of the request>/callback\"; set it explicitly behind a proxy",
|
|
86
|
+
"description": "Callback url registered at the provider",
|
|
91
87
|
"type": "string"
|
|
92
88
|
},
|
|
93
89
|
"redirects": {
|
|
94
|
-
"$ref": "#/definitions/
|
|
90
|
+
"$ref": "#/definitions/OAuthRedirects",
|
|
91
|
+
"description": "Browser redirects of the callback"
|
|
95
92
|
},
|
|
96
93
|
"scope": {
|
|
97
|
-
"
|
|
98
|
-
"
|
|
94
|
+
"default": [
|
|
95
|
+
"openid",
|
|
96
|
+
"email",
|
|
97
|
+
"profile"
|
|
99
98
|
],
|
|
100
|
-
"description": "
|
|
99
|
+
"description": "Scopes requested from Google",
|
|
101
100
|
"items": {
|
|
102
101
|
"type": "string"
|
|
103
102
|
},
|
|
104
103
|
"type": "array"
|
|
105
104
|
},
|
|
105
|
+
"trustEmailVerification": {
|
|
106
|
+
"anyOf": [
|
|
107
|
+
{
|
|
108
|
+
"items": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
111
|
+
"type": "array"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"type": "boolean"
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"description": "Whether the verification of the email is believed (see ProviderEmailPolicy)"
|
|
118
|
+
},
|
|
106
119
|
"type": {
|
|
107
120
|
"description": "Type of the service",
|
|
108
121
|
"type": "string"
|
|
109
122
|
},
|
|
110
123
|
"url": {
|
|
111
|
-
"
|
|
112
|
-
"description": "
|
|
124
|
+
"default": "/auth/<providerName>",
|
|
125
|
+
"description": "Route prefix of the login (`<url>`) and callback (`<url>/callback`) routes",
|
|
113
126
|
"type": "string"
|
|
114
127
|
},
|
|
115
128
|
"openapi": {
|
|
@@ -120,16 +133,12 @@
|
|
|
120
133
|
"required": [
|
|
121
134
|
"client_id",
|
|
122
135
|
"client_secret",
|
|
123
|
-
"scope",
|
|
124
136
|
"type"
|
|
125
137
|
],
|
|
126
138
|
"type": "object",
|
|
127
139
|
"title": "GoogleAuthentication"
|
|
128
140
|
},
|
|
129
|
-
"Configuration": "lib/google-auth.service:GoogleParameters"
|
|
130
|
-
"capabilities": [
|
|
131
|
-
"request-filter"
|
|
132
|
-
]
|
|
141
|
+
"Configuration": "lib/google-auth.service:GoogleParameters"
|
|
133
142
|
}
|
|
134
143
|
},
|
|
135
144
|
"models": {},
|
|
@@ -140,10 +149,6 @@
|
|
|
140
149
|
"additionalProperties": false,
|
|
141
150
|
"description": "Represent a file to store",
|
|
142
151
|
"properties": {
|
|
143
|
-
"challenge": {
|
|
144
|
-
"description": "Will be computed by the service\n\nhash of the content prefixed by 'WEBDA'",
|
|
145
|
-
"type": "string"
|
|
146
|
-
},
|
|
147
152
|
"hash": {
|
|
148
153
|
"description": "Will be computed by the service\n\nhash of the content",
|
|
149
154
|
"type": "string"
|
|
@@ -175,6 +180,31 @@
|
|
|
175
180
|
],
|
|
176
181
|
"type": "object",
|
|
177
182
|
"title": "BinaryFile"
|
|
183
|
+
},
|
|
184
|
+
"Webda/OAuthTokenRequest": {
|
|
185
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
186
|
+
"WebdaSchema": true,
|
|
187
|
+
"additionalProperties": false,
|
|
188
|
+
"definitions": {
|
|
189
|
+
"OAuthTokens": {
|
|
190
|
+
"additionalProperties": {},
|
|
191
|
+
"description": "Credentials as returned by an OAuth token endpoint (google-auth-library `Credentials` shape): `id_token`,\n`access_token`, `refresh_token`, `expiry_date`, `token_type`, `scope`... Values are not typed by the schema\n(providers send nulls); providers check what they use.",
|
|
192
|
+
"type": "object"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"description": "Input of the `Auth.<Provider>.Token` operations: `token`, or the provider credentials `tokens`",
|
|
196
|
+
"properties": {
|
|
197
|
+
"token": {
|
|
198
|
+
"description": "Token issued by the provider to the client (an ID token for OpenID Connect providers)",
|
|
199
|
+
"type": "string"
|
|
200
|
+
},
|
|
201
|
+
"tokens": {
|
|
202
|
+
"$ref": "#/definitions/OAuthTokens",
|
|
203
|
+
"description": "Credentials obtained by the client from the provider (v3 body; its `id_token` is verified)"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"type": "object",
|
|
207
|
+
"title": "OAuthTokenRequest"
|
|
178
208
|
}
|
|
179
209
|
},
|
|
180
210
|
"behaviors": {}
|