diff --git a/docs/blog/version-4.5-release-notes.md b/docs/blog/version-4.5-release-notes.md index c67585fcec..0e84255da2 100644 --- a/docs/blog/version-4.5-release-notes.md +++ b/docs/blog/version-4.5-release-notes.md @@ -43,6 +43,36 @@ export class SubscriptionService { ``` +## Social authentication for SPAs + +If you wish to manually manage the redirection to the consent page on the client side (which is often necessary when developing an SPA), you can now do so with the `createHttpResponseWithConsentPageUrl` method. It returns an `HttpResponseOK` whose body contains the URL of the consent page. + +```typescript +export class AuthController { + @dependency + google: GoogleProvider; + + @Get('/signin/google') + getConsentPageURL() { + return this.google.createHttpResponseWithConsentPageUrl(); + } + + // ... + +} + +``` + +## Google social authentification + +The typing of the `GoogleProvider` service has been improved. The `userInfo` property returned by `getUserInfo` is now typed with the values returned by the Google server. + +```typescript +const { userInfo } = await this.googleProvider.getUserInfo(...); + +// userInfo.email, userInfo.family_name, etc +``` + ## Logging improvements In previous versions, the util function `displayServerURL` and configuration errors printed logs on several lines, which was not appropriate for logging software. diff --git a/docs/docs/authentication/social-auth.md b/docs/docs/authentication/social-auth.md index 729a756259..588c1549be 100644 --- a/docs/docs/authentication/social-auth.md +++ b/docs/docs/authentication/social-auth.md @@ -137,7 +137,7 @@ export class AuthController { redirectToGoogle() { // Your "Login In with Google" button should point to this route. // The user will be redirected to Google auth page. - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/google/callback') @@ -157,11 +157,13 @@ export class AuthController { You can also override in the `redirect` method the scopes you want: ```typescript -return this.google.redirect({ scopes: [ 'email' ] }); +return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true, scopes: [ 'email' ] }); ``` Additional parameters can passed to the `redirect` and `getUserInfo` methods depending on the provider. +> If you want to manage the redirection on the client side manually, don't specify the `isRedirection` option. In this case, the `createHttpResponseWithConsentPageUrl` method returns an `HttpResponseOK` whose body contains the URL of the consent page. The name of the body property is `consentPageUrl`. + ## Techniques ### Usage with sessions @@ -210,7 +212,7 @@ export class AuthController { @Get('/signin/google') redirectToGoogle() { - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/google/callback') @@ -218,7 +220,7 @@ export class AuthController { cookie: true, }) async handleGoogleRedirection(ctx: Context) { - const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx); + const { userInfo } = await this.google.getUserInfo(ctx); if (!userInfo.email) { throw new Error('Google should have returned an email address.'); @@ -285,12 +287,12 @@ export class AuthController { @Get('/signin/google') redirectToGoogle() { - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/google/callback') async handleGoogleRedirection(ctx: Context) { - const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx); + const { userInfo } = await this.google.getUserInfo(ctx); if (!userInfo.email) { throw new Error('Google should have returned an email address.'); @@ -442,11 +444,11 @@ Visit the [Google API Console](https://console.developers.google.com/apis/creden #### Redirection parameters -The `redirect` method of the `GoogleProvider` accepts additional parameters. These parameters and their description are listed [here](https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters) and are all optional. +The `createHttpResponseWithConsentPageUrl` method of the `GoogleProvider` accepts additional parameters. These parameters and their description are listed [here](https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters) and are all optional. *Example* ```typescript -this.google.redirect({ /* ... */ }, { +this.google.createHttpResponseWithConsentPageUrl({ /* ... */ }, { access_type: 'offline' }) ``` @@ -463,11 +465,11 @@ Visit [Facebook's developer website](https://developers.facebook.com/) to create #### Redirection parameters -The `redirect` method of the `FacebookProvider` accepts an additional `auth_type` parameter which is optional. +The `createHttpResponseWithConsentPageUrl` method of the `FacebookProvider` accepts an additional `auth_type` parameter which is optional. *Example* ```typescript -this.facebook.redirect({ /* ... */ }, { +this.facebook.createHttpResponseWithConsentPageUrl({ /* ... */ }, { auth_type: 'rerequest' }); ``` @@ -505,11 +507,11 @@ Additional documentation on Github's redirect URLs can be found [here](https://d #### Redirection parameters -The `redirect` method of the `GithubProvider` accepts additional parameters. These parameters and their description are listed below and are all optional. +The `createHttpResponseWithConsentPageUrl` method of the `GithubProvider` accepts additional parameters. These parameters and their description are listed below and are all optional. *Example* ```typescript -this.github.redirect({ /* ... */ }, { +this.github.createHttpResponseWithConsentPageUrl({ /* ... */ }, { allow_signup: false }) ``` diff --git a/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md b/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md index b0609ca731..9cef3a172d 100644 --- a/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md +++ b/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md @@ -132,7 +132,7 @@ export class SocialAuthController { @Get('/google') redirectToGoogle() { - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/google/callback') diff --git a/packages/acceptance-tests/src/docs/authentication/social-auth/adding-social-auth-controllers.feature.ts b/packages/acceptance-tests/src/docs/authentication/social-auth/adding-social-auth-controllers.feature.ts index 73a59280fb..9085c5d7c7 100644 --- a/packages/acceptance-tests/src/docs/authentication/social-auth/adding-social-auth-controllers.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication/social-auth/adding-social-auth-controllers.feature.ts @@ -19,7 +19,7 @@ describe('Feature: Adding social auth controllers', () => { redirectToGoogle() { // Your "Login In with Google" button should point to this route. // The user will be redirected to Google auth page. - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/google/callback') diff --git a/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-jwt.feature.ts b/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-jwt.feature.ts index 1d1bc8ba24..b38e8e3185 100644 --- a/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-jwt.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-jwt.feature.ts @@ -56,12 +56,12 @@ describe('Feature: Using social auth with JWT', () => { @Get('/signin/google') redirectToGoogle() { - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/google/callback') async handleGoogleRedirection(ctx: Context) { - const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx); + const { userInfo } = await this.google.getUserInfo(ctx); if (!userInfo.email) { throw new Error('Google should have returned an email address.'); diff --git a/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-sessions.feature.ts b/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-sessions.feature.ts index 936e7f3e8b..8e934743bd 100644 --- a/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-sessions.feature.ts +++ b/packages/acceptance-tests/src/docs/authentication/social-auth/using-social-auth-with-sessions.feature.ts @@ -59,7 +59,7 @@ describe('Feature: Using social auth with sessions', () => { @Get('/signin/google') redirectToGoogle() { - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/google/callback') @@ -67,7 +67,7 @@ describe('Feature: Using social auth with sessions', () => { cookie: true, }) async handleGoogleRedirection(ctx: Context) { - const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx); + const { userInfo } = await this.google.getUserInfo(ctx); if (!userInfo.email) { throw new Error('Google should have returned an email address.'); diff --git a/packages/examples/src/app/controllers/auth.controller.ts b/packages/examples/src/app/controllers/auth.controller.ts index c3b7d76711..3a2bfc7f43 100644 --- a/packages/examples/src/app/controllers/auth.controller.ts +++ b/packages/examples/src/app/controllers/auth.controller.ts @@ -32,7 +32,7 @@ export class AuthController { @Get('/signin/google') redirectToGoogle() { - return this.google.redirect(); + return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/google/cb') @@ -43,7 +43,7 @@ export class AuthController { @Get('/signin/facebook') redirectToFacebook() { - return this.facebook.redirect(); + return this.facebook.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/facebook/cb') @@ -54,7 +54,7 @@ export class AuthController { @Get('/signin/github') redirectToGithub() { - return this.github.redirect(); + return this.github.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/github/cb') @@ -65,7 +65,7 @@ export class AuthController { @Get('/signin/linkedin') redirectToLinkedIn() { - return this.linkedin.redirect(); + return this.linkedin.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/linkedin/cb') @@ -76,7 +76,7 @@ export class AuthController { @Get('/signin/twitter') redirectToTwitter() { - return this.twitter.redirect(); + return this.twitter.createHttpResponseWithConsentPageUrl({ isRedirection: true }); } @Get('/signin/twitter/cb') diff --git a/packages/social/src/abstract-provider.service.spec.ts b/packages/social/src/abstract-provider.service.spec.ts index 8cdbabe0db..0275a36c35 100644 --- a/packages/social/src/abstract-provider.service.spec.ts +++ b/packages/social/src/abstract-provider.service.spec.ts @@ -13,6 +13,7 @@ import { createService, HttpResponseBadRequest, HttpResponseOK, + isHttpResponseOK, isHttpResponseRedirect, Post } from '@foal/core'; @@ -177,156 +178,209 @@ describe('AbstractProvider', () => { Config.remove('settings.social.cookie.domain'); }); - describe('has a "redirect" method that', () => { - - it('should return an HttpResponseRedirect object.', async () => { - const result = await provider.redirect(); - strictEqual(isHttpResponseRedirect(result), true); - }); + describe('has a "createHttpResponseWithConsentPageUrl" method that', () => { - describe('should return an HttpResponseRedirect object', () => { + context('given the isRedirection option is false or not defined', () => { - it('with a redirect path which contains a client ID, a response type, a redirect URI.', async () => { - const response = await provider.redirect(); - ok(response.path.startsWith( - 'https://example2.com/auth?' - + 'response_type=code&' - + 'client_id=clientIdXXX&' - + 'redirect_uri=https%3A%2F%2Fexample.com%2Fcallback' - )); + it('should return an HttpResponseOK object.', async () => { + const result = await provider.createHttpResponseWithConsentPageUrl(); + strictEqual(isHttpResponseOK(result), true); }); - it('with a redirect path which does not contain a scope if none was provided.', async () => { - const response = await provider.redirect(); - const searchParams = new URLSearchParams(response.path); + describe('should return an HttpResponseOK object', () => { - strictEqual(searchParams.get('scope'), null); - }); + it('with a consentPageUrl which contains a client ID, a response type, a redirect URI.', async () => { + const response = await provider.createHttpResponseWithConsentPageUrl(); - it('with a redirect path which contains the scopes if any are provided by the class.', async () => { - class ConcreteProvider2 extends ConcreteProvider { - defaultScopes = [ 'scope1', 'scope2' ]; - } - provider = createService(ConcreteProvider2); + ok(response.body.consentPageUrl.startsWith( + 'https://example2.com/auth?' + + 'response_type=code&' + + 'client_id=clientIdXXX&' + + 'redirect_uri=https%3A%2F%2Fexample.com%2Fcallback' + )); + }); - const response = await provider.redirect(); - const searchParams = new URLSearchParams(response.path); + it('with a consentPageUrl which does not contain a scope if none was provided.', async () => { + const response = await provider.createHttpResponseWithConsentPageUrl(); + const searchParams = new URLSearchParams(response.body.consentPageUrl); - strictEqual(searchParams.get('scope'), 'scope1 scope2'); - }); + strictEqual(searchParams.get('scope'), null); + }); - it('with a redirect path which contains the scopes if any are provided by the class' - + ' (custom separator).', async () => { - class ConcreteProvider2 extends ConcreteProvider { - defaultScopes = [ 'scope1', 'scope2' ]; - scopeSeparator = ','; - } - provider = createService(ConcreteProvider2); + it('with a consentPageUrl which contains the scopes if any are provided by the class.', async () => { + class ConcreteProvider2 extends ConcreteProvider { + defaultScopes = [ 'scope1', 'scope2' ]; + } + provider = createService(ConcreteProvider2); - const response = await provider.redirect(); - const searchParams = new URLSearchParams(response.path); + const response = await provider.createHttpResponseWithConsentPageUrl(); + const searchParams = new URLSearchParams(response.body.consentPageUrl); - strictEqual(searchParams.get('scope'), 'scope1,scope2'); - }); + strictEqual(searchParams.get('scope'), 'scope1 scope2'); + }); - it('with a redirect path which contains the scopes if any are provided to the method.', async () => { - class ConcreteProvider2 extends ConcreteProvider { - // This checks that the default scopes will be override. - defaultScopes = [ 'scope1', 'scope2' ]; - } - provider = createService(ConcreteProvider2); + it('with a consentPageUrl which contains the scopes if any are provided by the class' + + ' (custom separator).', async () => { + class ConcreteProvider2 extends ConcreteProvider { + defaultScopes = [ 'scope1', 'scope2' ]; + scopeSeparator = ','; + } + provider = createService(ConcreteProvider2); + + const response = await provider.createHttpResponseWithConsentPageUrl(); + const searchParams = new URLSearchParams(response.body.consentPageUrl); - const response = await provider.redirect({ - scopes: [ 'scope3', 'scope4' ] + strictEqual(searchParams.get('scope'), 'scope1,scope2'); }); - const searchParams = new URLSearchParams(response.path); - strictEqual(searchParams.get('scope'), 'scope3 scope4'); - }); + it('with a consentPageUrl which contains the scopes if any are provided to the method.', async () => { + class ConcreteProvider2 extends ConcreteProvider { + // This checks that the default scopes will be override. + defaultScopes = [ 'scope1', 'scope2' ]; + } + provider = createService(ConcreteProvider2); - it('with a generated state to protect against CSRF attacks.', async () => { - const response = await provider.redirect(); - const stateCookieValue = response.getCookie(STATE_COOKIE_NAME).value; - const stateCookieOptions = response.getCookie(STATE_COOKIE_NAME).options; - if (typeof stateCookieValue !== 'string') { - throw new Error('Cookie not found.'); - } + const response = await provider.createHttpResponseWithConsentPageUrl({ + scopes: [ 'scope3', 'scope4' ] + }); + const searchParams = new URLSearchParams(response.body.consentPageUrl); - deepStrictEqual(stateCookieOptions, { - httpOnly: true, - maxAge: 300, - path: '/', - secure: false + strictEqual(searchParams.get('scope'), 'scope3 scope4'); }); - const searchParams = new URLSearchParams(response.path); - const stateParamValue = searchParams.get('state'); - if (typeof stateParamValue !== 'string') { - throw new Error('State parameter not found.'); - } + it('with a generated state to protect against CSRF attacks.', async () => { + const response = await provider.createHttpResponseWithConsentPageUrl(); + const stateCookieValue = response.getCookie(STATE_COOKIE_NAME).value; + const stateCookieOptions = response.getCookie(STATE_COOKIE_NAME).options; + if (typeof stateCookieValue !== 'string') { + throw new Error('Cookie not found.'); + } - strictEqual(stateParamValue, stateCookieValue); - notStrictEqual(stateCookieValue.length, 0); - }); + deepStrictEqual(stateCookieOptions, { + httpOnly: true, + maxAge: 300, + path: '/', + secure: false + }); - it('with a generated state with does not contain problematic URL characters.', async () => { - // This test is bad because it is not deterministic. - // Unfortunately, since the state is randomly generated, we can't do better. + const searchParams = new URLSearchParams(response.body.consentPageUrl); + const stateParamValue = searchParams.get('state'); + if (typeof stateParamValue !== 'string') { + throw new Error('State parameter not found.'); + } - const response = await provider.redirect(); + strictEqual(stateParamValue, stateCookieValue); + notStrictEqual(stateCookieValue.length, 0); + }); - const searchParams = new URLSearchParams(response.path); - const stateParamValue = searchParams.get('state'); - if (typeof stateParamValue !== 'string') { - throw new Error('State parameter not found.'); - } + it('with a generated state with does not contain problematic URL characters.', async () => { + // This test is bad because it is not deterministic. + // Unfortunately, since the state is randomly generated, we can't do better. - strictEqual(stateParamValue.includes('+'), false); - strictEqual(stateParamValue.includes('/'), false); - strictEqual(stateParamValue.includes('='), false); - }); + const response = await provider.createHttpResponseWithConsentPageUrl(); + const searchParams = new URLSearchParams(response.body.consentPageUrl); + const stateParamValue = searchParams.get('state'); + if (typeof stateParamValue !== 'string') { + throw new Error('State parameter not found.'); + } - it('with a generated state in a cookie whose secure option is defined with the config.', async () => { - Config.set('settings.social.cookie.secure', true); + strictEqual(stateParamValue.includes('+'), false); + strictEqual(stateParamValue.includes('/'), false); + strictEqual(stateParamValue.includes('='), false); + }); - const response = await provider.redirect(); - const { options } = response.getCookie(STATE_COOKIE_NAME); + it('with a generated state in a cookie whose secure option is defined with the config.', async () => { + Config.set('settings.social.cookie.secure', true); - strictEqual(options.secure, true); - }); + const response = await provider.createHttpResponseWithConsentPageUrl(); + const { options } = response.getCookie(STATE_COOKIE_NAME); - it('with a generated state in a cookie whose domain option is defined with the config.', async () => { - Config.set('settings.social.cookie.domain', 'foalts.org'); + strictEqual(options.secure, true); + }); - const response = await provider.redirect(); - const { options } = response.getCookie(STATE_COOKIE_NAME); + it('with a generated state in a cookie whose domain option is defined with the config.', async () => { + Config.set('settings.social.cookie.domain', 'foalts.org'); - strictEqual(options.domain, 'foalts.org'); - }); + const response = await provider.createHttpResponseWithConsentPageUrl(); + const { options } = response.getCookie(STATE_COOKIE_NAME); + + strictEqual(options.domain, 'foalts.org'); + }); - it('with a redirect path which contains extra parameters if any are provided to the method.', async () => { - provider = createService(ConcreteProvider); + it('with a consentPageUrl which contains extra parameters if any are provided to the method.', async () => { + provider = createService(ConcreteProvider); - const response = await provider.redirect({}, { foo: 'bar2' }); - const searchParams = new URLSearchParams(response.path); + const response = await provider.createHttpResponseWithConsentPageUrl({}, { foo: 'bar2' }); + const searchParams = new URLSearchParams(response.body.consentPageUrl); + + strictEqual(searchParams.get('foo'), 'bar2'); + }); + + it('with a consentPageUrl that do NOT contain PKCE parameters.', async () => { + provider = createService(ConcreteProvider); + + const response = await provider.createHttpResponseWithConsentPageUrl(); + const searchParams = new URLSearchParams(response.body.consentPageUrl); + + strictEqual(searchParams.get('code_challenge'), null); + strictEqual(searchParams.get('code_challenge_method'), null); + }); - strictEqual(searchParams.get('foo'), 'bar2'); }); - it('with a redirect path that do NOT contain PKCE parameters.', async () => { - provider = createService(ConcreteProvider); + }); + + context('given the isRedirection option is true', () => { - const response = await provider.redirect(); - const searchParams = new URLSearchParams(response.path); + it('should return an HttpResponseRedirect object where the path is the consent page URL.', async () => { + const httpResponseOK = await provider.createHttpResponseWithConsentPageUrl(); + const httpResponseRedirect = await provider.createHttpResponseWithConsentPageUrl({ isRedirection: true }); - strictEqual(searchParams.get('code_challenge'), null); - strictEqual(searchParams.get('code_challenge_method'), null); + if (!isHttpResponseRedirect(httpResponseRedirect)) { + throw new Error('The response should be an HttpResponseRedirect object.'); + } + + const httpResponseOKConsentPageUrl = new URL(httpResponseOK.body.consentPageUrl); + const httpResponseRedirectConsentPageUrl = new URL(httpResponseRedirect.path); + + // Remove values generated randomly. + httpResponseOKConsentPageUrl.searchParams.delete('state'); + httpResponseRedirectConsentPageUrl.searchParams.delete('state'); + + strictEqual(httpResponseRedirectConsentPageUrl.href, httpResponseOKConsentPageUrl.href); + notStrictEqual(httpResponseRedirect.getCookie(STATE_COOKIE_NAME), undefined); }); }); }); + describe('has a "redirect" method that', () => { + + it('should behave like the "createHttpResponseWithConsentPageUrl" method with the isRedirection option set to true.', async () => { + const actual = await provider.redirect({ scopes: ['foo'] }); + const expected = await provider.createHttpResponseWithConsentPageUrl({ scopes: ['foo'], isRedirection: true }); + + if (!isHttpResponseRedirect(actual)) { + throw new Error('The response should be an HttpResponseRedirect object.'); + } + + if (!isHttpResponseRedirect(expected)) { + throw new Error('The response should be an HttpResponseRedirect object.'); + } + + const actualConsentPageUrl = new URL(actual.path); + const expectedConsentPageUrl = new URL(expected.path); + + // Remove values generated randomly. + actualConsentPageUrl.searchParams.delete('state'); + expectedConsentPageUrl.searchParams.delete('state'); + + strictEqual(actualConsentPageUrl.href, expectedConsentPageUrl.href); + }); + + }); + describe('has a "getTokens" method that', () => { let server: Server; diff --git a/packages/social/src/abstract-provider.service.ts b/packages/social/src/abstract-provider.service.ts index 0484f90794..593c6ce7f1 100644 --- a/packages/social/src/abstract-provider.service.ts +++ b/packages/social/src/abstract-provider.service.ts @@ -3,7 +3,7 @@ import { URL, URLSearchParams } from 'url'; import * as crypto from 'crypto'; // 3p -import { Config, Context, generateToken, HttpResponseRedirect, convertBase64ToBase64url, CookieOptions } from '@foal/core'; +import { Config, Context, generateToken, HttpResponseRedirect, convertBase64ToBase64url, CookieOptions, HttpResponseOK } from '@foal/core'; import * as fetch from 'node-fetch'; /** @@ -114,8 +114,9 @@ export interface ObjectType { * @class AbstractProvider * @template AuthParameters - Additional parameters to pass to the auth endpoint. * @template UserInfoParameters - Additional parameters to pass when retrieving user information. + * @template IUserInfo - Type of the user information. */ -export abstract class AbstractProvider { +export abstract class AbstractProvider { /** * Configuration paths from which the client ID, client secret and redirect URI must be retrieved. @@ -238,14 +239,19 @@ export abstract class AbstractProvider} The HttpResponseRedirect object. + * @returns {Promise} The HttpResponseOK or HttpResponseRedirect object. * @memberof AbstractProvider */ - async redirect({ scopes }: { scopes?: string[] } = {}, params?: AuthParameters): Promise { + async createHttpResponseWithConsentPageUrl({ scopes, isRedirection }: { scopes?: string[], isRedirection?: boolean } = {}, params?: AuthParameters): Promise { // Build the authorization URL. const url = new URL(this.authEndpoint); url.searchParams.set('response_type', 'code'); @@ -278,7 +284,7 @@ export abstract class AbstractProvider} The HttpResponseRedirect object. + * @memberof AbstractProvider + * @deprecated + */ + async redirect({ scopes }: { scopes?: string[] } = {}, params?: AuthParameters): Promise { + return this.createHttpResponseWithConsentPageUrl({ scopes, isRedirection: true }, params) as Promise; + } + /** * Function to use in the controller method that handles the provider redirection. * @@ -379,7 +400,7 @@ export abstract class AbstractProvider>} The access token and the user information * @memberof AbstractProvider */ - async getUserInfo(ctx: Context, params?: UserInfoParameters): Promise> { + async getUserInfo(ctx: Context, params?: UserInfoParameters): Promise> { const tokens = await this.getTokens(ctx); const userInfo = await this.getUserInfoFromTokens(tokens, params); return { userInfo, tokens }; diff --git a/packages/social/src/google-provider.service.ts b/packages/social/src/google-provider.service.ts index ea69ca14be..faf5c4ead2 100644 --- a/packages/social/src/google-provider.service.ts +++ b/packages/social/src/google-provider.service.ts @@ -13,6 +13,28 @@ export interface GoogleAuthParams { hd?: string; } +// https://developers.google.com/identity/openid-connect/openid-connect#an-id-tokens-payload +export interface GoogleUserInfo { + aud: string; + exp: number; + iat: number; + iss: string; + sub: string; + at_hash?: string; + azp?: string; + email?: string; + email_verified?: boolean; + family_name?: string; + given_name?: string; + hd?: string; + locale?: string; + name?: string; + nonce?: string; + picture?: string; + profile?: string; + [name: string]: unknown; +} + export class InvalidJWTError extends Error { readonly name = 'InvalidJWTError'; } @@ -24,7 +46,7 @@ export class InvalidJWTError extends Error { * @class GoogleProvider * @extends {AbstractProvider} */ -export class GoogleProvider extends AbstractProvider { +export class GoogleProvider extends AbstractProvider { protected configPaths = { clientId: 'settings.social.google.clientId', clientSecret: 'settings.social.google.clientSecret', diff --git a/packages/social/src/index.ts b/packages/social/src/index.ts index b0e63b8361..72ab6d4f6f 100644 --- a/packages/social/src/index.ts +++ b/packages/social/src/index.ts @@ -27,6 +27,7 @@ export { GoogleProvider, InvalidJWTError, GoogleAuthParams, + GoogleUserInfo, } from './google-provider.service'; export { LinkedInProvider,