From 82e34e07ebc54f176072befe68bc900fc862cc25 Mon Sep 17 00:00:00 2001 From: Aaron Ware Date: Sun, 14 Apr 2024 16:35:23 +0000 Subject: [PATCH 1/6] improve(#453): Allow for state to be passed to AuthFlow --- packages/oauth-providers/src/providers/google/googleAuth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/oauth-providers/src/providers/google/googleAuth.ts b/packages/oauth-providers/src/providers/google/googleAuth.ts index 43e5de0d..0546b0ff 100644 --- a/packages/oauth-providers/src/providers/google/googleAuth.ts +++ b/packages/oauth-providers/src/providers/google/googleAuth.ts @@ -11,6 +11,7 @@ export function googleAuth(options: { prompt?: 'none' | 'consent' | 'select_account' client_id?: string client_secret?: string + state?: string }): MiddlewareHandler { return async (c, next) => { const newState = getRandomState() @@ -22,7 +23,7 @@ export function googleAuth(options: { login_hint: options.login_hint, prompt: options.prompt, scope: options.scope, - state: newState, + state: (options.state as string) || newState, code: c.req.query('code'), token: { token: c.req.query('access_token') as string, From fc1ea979cbbcce175843b16ecc9d7027caca1662 Mon Sep 17 00:00:00 2001 From: Aaron Ware Date: Sun, 14 Apr 2024 16:37:11 +0000 Subject: [PATCH 2/6] fix(#453): No need to cast as string --- packages/oauth-providers/src/providers/google/googleAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/oauth-providers/src/providers/google/googleAuth.ts b/packages/oauth-providers/src/providers/google/googleAuth.ts index 0546b0ff..a7637730 100644 --- a/packages/oauth-providers/src/providers/google/googleAuth.ts +++ b/packages/oauth-providers/src/providers/google/googleAuth.ts @@ -23,7 +23,7 @@ export function googleAuth(options: { login_hint: options.login_hint, prompt: options.prompt, scope: options.scope, - state: (options.state as string) || newState, + state: options.state || newState, code: c.req.query('code'), token: { token: c.req.query('access_token') as string, From 980d0f07918f77fe8682a7bdc9d53dae8e269324 Mon Sep 17 00:00:00 2001 From: Aaron Ware Date: Wed, 17 Apr 2024 12:21:34 -0400 Subject: [PATCH 3/6] chore(#453): Create changeset Closes #453 --- .changeset/green-files-thank.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/green-files-thank.md diff --git a/.changeset/green-files-thank.md b/.changeset/green-files-thank.md new file mode 100644 index 00000000..3646db72 --- /dev/null +++ b/.changeset/green-files-thank.md @@ -0,0 +1,5 @@ +--- +'@hono/oauth-providers': minor +--- + +Allow for an optional state arg to be passed to Google Auth middleware From 660ffb87404520d530869615ad369b5ed8479cc9 Mon Sep 17 00:00:00 2001 From: Aaron Ware Date: Wed, 17 Apr 2024 19:19:03 -0400 Subject: [PATCH 4/6] fix(#453): Needed to take CSRF into account --- packages/oauth-providers/src/providers/google/googleAuth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/oauth-providers/src/providers/google/googleAuth.ts b/packages/oauth-providers/src/providers/google/googleAuth.ts index a7637730..1ba22493 100644 --- a/packages/oauth-providers/src/providers/google/googleAuth.ts +++ b/packages/oauth-providers/src/providers/google/googleAuth.ts @@ -14,7 +14,7 @@ export function googleAuth(options: { state?: string }): MiddlewareHandler { return async (c, next) => { - const newState = getRandomState() + const newState = options.state || getRandomState() // Create new Auth instance const auth = new AuthFlow({ client_id: options.client_id || (c.env?.GOOGLE_ID as string), @@ -23,7 +23,7 @@ export function googleAuth(options: { login_hint: options.login_hint, prompt: options.prompt, scope: options.scope, - state: options.state || newState, + state: newState, code: c.req.query('code'), token: { token: c.req.query('access_token') as string, From 378802a8cfe1cf6c04318bccd4bc92059523db2c Mon Sep 17 00:00:00 2001 From: Aaron Ware Date: Wed, 10 Jul 2024 08:52:04 -0400 Subject: [PATCH 5/6] #619 Adding support for access_type --- .../src/providers/google/authFlow.ts | 50 ++++++++++++++++++- .../src/providers/google/googleAuth.ts | 6 +++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/oauth-providers/src/providers/google/authFlow.ts b/packages/oauth-providers/src/providers/google/authFlow.ts index a5fe6d01..62184c6b 100644 --- a/packages/oauth-providers/src/providers/google/authFlow.ts +++ b/packages/oauth-providers/src/providers/google/authFlow.ts @@ -14,6 +14,7 @@ type GoogleAuthFlow = { state?: string login_hint?: string prompt?: 'none' | 'consent' | 'select_account' + access_type?: 'online' | 'offline' } export class AuthFlow { @@ -26,6 +27,7 @@ export class AuthFlow { state: string | undefined login_hint: string | undefined prompt: 'none' | 'consent' | 'select_account' | undefined + access_type: 'online' | 'offline' | undefined user: Partial | undefined granted_scopes: string[] | undefined @@ -39,6 +41,7 @@ export class AuthFlow { state, code, token, + access_type }: GoogleAuthFlow) { this.client_id = client_id this.client_secret = client_secret @@ -49,6 +52,7 @@ export class AuthFlow { this.state = state this.code = code this.token = token + this.access_type = access_type this.user = undefined this.granted_scopes = undefined @@ -71,6 +75,7 @@ export class AuthFlow { include_granted_scopes: true, scope: this.scope.join(' '), state: this.state, + access_type: this.access_type }) return `https://accounts.google.com/o/oauth2/v2/auth?${parsedOptions}` } @@ -99,6 +104,7 @@ export class AuthFlow { this.token = { token: response.access_token, expires_in: response.expires_in, + refresh_token: reponse.refresh_token } this.granted_scopes = response.scope.split(' ') @@ -106,7 +112,13 @@ export class AuthFlow { } async getUserData() { - await this.getTokenFromCode() + + // Check if token is expired and refresh if necessary + if ( this.access_type === 'offline' && this.isTokenExpired() ) { + await this.refreshToken() + } else { + await this.getTokenFromCode() + } const response = (await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { @@ -122,4 +134,40 @@ export class AuthFlow { this.user = response } } + + async refreshToken() { + if (!this.token?.refresh_token) { + throw new HTTPException(400, { message: 'Refresh token not found' }) + } + + const response = (await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json', + }, + body: JSON.stringify({ + client_id: this.client_id, + client_secret: this.client_secret, + refresh_token: this.token.refresh_token, + grant_type: 'refresh_token', + }), + }).then((res) => res.json())) as GoogleTokenResponse | GoogleErrorResponse + + if ('error' in response) { + throw new HTTPException(400, { message: response.error_description }) + } + + if ('access_token' in response) { + this.token.token = response.access_token + this.token.expires_in = response.expires_in + } + } + + isTokenExpired() { + const currentTime = Math.floor(Date.now() / 1000) + return currentTime >= (this.token?.expires_in || 0) + } + + } diff --git a/packages/oauth-providers/src/providers/google/googleAuth.ts b/packages/oauth-providers/src/providers/google/googleAuth.ts index bad111a8..3c252111 100644 --- a/packages/oauth-providers/src/providers/google/googleAuth.ts +++ b/packages/oauth-providers/src/providers/google/googleAuth.ts @@ -13,6 +13,7 @@ export function googleAuth(options: { client_id?: string client_secret?: string state?: string + access_type?: string }): MiddlewareHandler { return async (c, next) => { const newState = options.state || getRandomState() @@ -30,6 +31,7 @@ export function googleAuth(options: { token: c.req.query('access_token') as string, expires_in: Number(c.req.query('expires-in')) as number, }, + access_type: options.access_type }) // Avoid CSRF attack by checking state @@ -59,6 +61,10 @@ export function googleAuth(options: { c.set('user-google', auth.user) c.set('granted-scopes', auth.granted_scopes) + if ( access_type === 'offline' ) { + c.set('refresh_token', auth.refresh_token ) + } + await next() } } From d6a1e09bb4038ddde1ea840122c7f8ca40f8d813 Mon Sep 17 00:00:00 2001 From: Aaron Ware Date: Wed, 10 Jul 2024 10:44:33 -0400 Subject: [PATCH 6/6] #619 Added refresh_token to responses for use w/ access_type=offline --- .changeset/cyan-balloons-lick.md | 5 +++++ packages/oauth-providers/src/providers/google/authFlow.ts | 6 +++--- .../oauth-providers/src/providers/google/googleAuth.ts | 7 ++----- packages/oauth-providers/src/providers/google/types.ts | 7 +++++++ 4 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 .changeset/cyan-balloons-lick.md diff --git a/.changeset/cyan-balloons-lick.md b/.changeset/cyan-balloons-lick.md new file mode 100644 index 00000000..dc8b8500 --- /dev/null +++ b/.changeset/cyan-balloons-lick.md @@ -0,0 +1,5 @@ +--- +'@hono/oauth-providers': minor +--- + +Allow for access_type to be passed for "offline" mode, defaults to "online". Allowing for refresh tokens to be used in subsequent requests diff --git a/packages/oauth-providers/src/providers/google/authFlow.ts b/packages/oauth-providers/src/providers/google/authFlow.ts index 62184c6b..0f03886e 100644 --- a/packages/oauth-providers/src/providers/google/authFlow.ts +++ b/packages/oauth-providers/src/providers/google/authFlow.ts @@ -1,8 +1,7 @@ import { HTTPException } from 'hono/http-exception' -import type { Token } from '../../types' import { toQueryParams } from '../../utils/objectToQuery' -import type { GoogleErrorResponse, GoogleTokenResponse, GoogleUser } from './types' +import type { GoogleErrorResponse, GoogleTokenResponse, GoogleUser, Token } from './types' type GoogleAuthFlow = { client_id: string @@ -104,7 +103,7 @@ export class AuthFlow { this.token = { token: response.access_token, expires_in: response.expires_in, - refresh_token: reponse.refresh_token + refresh_token: response.refresh_token } this.granted_scopes = response.scope.split(' ') @@ -161,6 +160,7 @@ export class AuthFlow { if ('access_token' in response) { this.token.token = response.access_token this.token.expires_in = response.expires_in + this.token.refresh_token = response.refresh_token } } diff --git a/packages/oauth-providers/src/providers/google/googleAuth.ts b/packages/oauth-providers/src/providers/google/googleAuth.ts index 3c252111..61b0cb3a 100644 --- a/packages/oauth-providers/src/providers/google/googleAuth.ts +++ b/packages/oauth-providers/src/providers/google/googleAuth.ts @@ -13,7 +13,7 @@ export function googleAuth(options: { client_id?: string client_secret?: string state?: string - access_type?: string + access_type?: 'online' | 'offline' }): MiddlewareHandler { return async (c, next) => { const newState = options.state || getRandomState() @@ -30,6 +30,7 @@ export function googleAuth(options: { token: { token: c.req.query('access_token') as string, expires_in: Number(c.req.query('expires-in')) as number, + refresh_token: c.req.query('refresh_token') as string, }, access_type: options.access_type }) @@ -61,10 +62,6 @@ export function googleAuth(options: { c.set('user-google', auth.user) c.set('granted-scopes', auth.granted_scopes) - if ( access_type === 'offline' ) { - c.set('refresh_token', auth.refresh_token ) - } - await next() } } diff --git a/packages/oauth-providers/src/providers/google/types.ts b/packages/oauth-providers/src/providers/google/types.ts index eafebf35..ae261ce2 100644 --- a/packages/oauth-providers/src/providers/google/types.ts +++ b/packages/oauth-providers/src/providers/google/types.ts @@ -13,6 +13,7 @@ export type GoogleTokenResponse = { scope: string token_type: string id_token: string + refresh_token: string } export type GoogleTokenInfoResponse = { @@ -36,3 +37,9 @@ export type GoogleUser = { picture: string locale: string } + +export type Token = { + token: string + expires_in: number + refresh_token: string +}