Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for refresh tokens to be used via access_type "offline" in Issue/#619 #634

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/cyan-balloons-lick.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .changeset/green-files-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/oauth-providers': minor
---

Allow for an optional state arg to be passed to Google Auth middleware
54 changes: 51 additions & 3 deletions packages/oauth-providers/src/providers/google/authFlow.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,6 +13,7 @@ type GoogleAuthFlow = {
state?: string
login_hint?: string
prompt?: 'none' | 'consent' | 'select_account'
access_type?: 'online' | 'offline'
}

export class AuthFlow {
Expand All @@ -26,6 +26,7 @@ export class AuthFlow {
state: string | undefined
login_hint: string | undefined
prompt: 'none' | 'consent' | 'select_account' | undefined
access_type: 'online' | 'offline' | undefined
user: Partial<GoogleUser> | undefined
granted_scopes: string[] | undefined

Expand All @@ -39,6 +40,7 @@ export class AuthFlow {
state,
code,
token,
access_type
}: GoogleAuthFlow) {
this.client_id = client_id
this.client_secret = client_secret
Expand All @@ -49,6 +51,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

Expand All @@ -71,6 +74,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}`
}
Expand Down Expand Up @@ -99,14 +103,21 @@ export class AuthFlow {
this.token = {
token: response.access_token,
expires_in: response.expires_in,
refresh_token: response.refresh_token
}

this.granted_scopes = response.scope.split(' ')
}
}

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: {
Expand All @@ -122,4 +133,41 @@ 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
this.token.refresh_token = response.refresh_token
}
}

isTokenExpired() {
const currentTime = Math.floor(Date.now() / 1000)
return currentTime >= (this.token?.expires_in || 0)
}


}
3 changes: 3 additions & 0 deletions packages/oauth-providers/src/providers/google/googleAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function googleAuth(options: {
client_id?: string
client_secret?: string
state?: string
access_type?: 'online' | 'offline'
}): MiddlewareHandler {
return async (c, next) => {
const newState = options.state || getRandomState()
Expand All @@ -29,7 +30,9 @@ 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
})

// Avoid CSRF attack by checking state
Expand Down
7 changes: 7 additions & 0 deletions packages/oauth-providers/src/providers/google/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type GoogleTokenResponse = {
scope: string
token_type: string
id_token: string
refresh_token: string
}

export type GoogleTokenInfoResponse = {
Expand All @@ -36,3 +37,9 @@ export type GoogleUser = {
picture: string
locale: string
}

export type Token = {
token: string
expires_in: number
refresh_token: string
}