Skip to content

Commit

Permalink
feat: add pkce (#591)
Browse files Browse the repository at this point in the history
The corresponding client side implementation for:
supabase/auth#891

Points of note:

- We don't support `plain` as a method on the client side for now. Users
can make use of `plain` on the server side endpoints if they wish.

TODO:
- [x] Add support for devices without a `window`

After PR:
- Update reference spec with example on how to use PKCE

---------

Co-authored-by: joel@joellee.org <joel@joellee.org>
Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>
Co-authored-by: Kang Ming <kang.ming1996@gmail.com>
  • Loading branch information
4 people committed Mar 29, 2023
1 parent 832d168 commit 2fc2781
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 7 deletions.
26 changes: 25 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
"docs:json": "typedoc --json docs/v2/spec.json --excludeExternals --excludePrivate --excludeProtected src/index.ts"
},
"dependencies": {
"cross-fetch": "^3.1.5"
"cross-fetch": "^3.1.5",
"crypto-js": "^4.1.1"
},
"devDependencies": {
"@types/crypto-js": "^4.1.1",
"@types/faker": "^5.1.6",
"@types/jest": "^28.1.6",
"@types/jsonwebtoken": "^8.5.6",
Expand Down
59 changes: 55 additions & 4 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ import {
uuid,
retryable,
sleep,
generatePKCEVerifier,
generatePKCEChallenge,
} from './lib/helpers'
import localStorageAdapter from './lib/local-storage'
import { polyfillGlobalThis } from './lib/polyfills'

import type {
AuthChangeEvent,
AuthResponse,
Expand Down Expand Up @@ -63,6 +66,7 @@ import type {
AuthenticatorAssuranceLevels,
Factor,
MFAChallengeAndVerifyParams,
OAuthFlowType,
} from './lib/types'

polyfillGlobalThis() // Make "globalThis" available
Expand Down Expand Up @@ -378,14 +382,43 @@ export default class GoTrueClient {
*/
async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
await this._removeSession()
return this._handleProviderSignIn(credentials.provider, {

return await this._handleProviderSignIn(credentials.provider, {
redirectTo: credentials.options?.redirectTo,
scopes: credentials.options?.scopes,
queryParams: credentials.options?.queryParams,
skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
flowType: credentials.options?.flowType ?? 'implicit',
})
}

/**
* Log in an existing user via a third-party provider.
*/
async exchangeCodeForSession(authCode: string): Promise<AuthResponse> {
const codeVerifier = await getItemAsync(this.storage, `${this.storageKey}-oauth-code-verifier`)
const { data, error } = await _request(
this.fetch,
'POST',
`${this.url}/token?grant_type=oauth_pkce`,
{
headers: this.headers,
body: {
auth_code: authCode,
code_verifier: codeVerifier,
},
xform: _sessionResponse,
}
)
await removeItemAsync(this.storage, `${this.storageKey}-oauth-code-verifier`)
if (error || !data) return { data: { user: null, session: null }, error }
if (data.session) {
await this._saveSession(data.session)
this._notifyAllSubscribers('SIGNED_IN', data.session)
}
return { data, error }
}

/**
* Allows signing in with an ID token issued by certain supported providers.
* The ID token is verified for validity and a new session is established.
Expand Down Expand Up @@ -1040,24 +1073,28 @@ export default class GoTrueClient {
return isValidSession
}

private _handleProviderSignIn(
private async _handleProviderSignIn(
provider: Provider,
options: {
redirectTo?: string
scopes?: string
queryParams?: { [key: string]: string }
skipBrowserRedirect?: boolean
flowType?: OAuthFlowType
} = {}
) {
const url: string = this._getUrlForProvider(provider, {

const url: string = await this._getUrlForProvider(provider, {
redirectTo: options.redirectTo,
scopes: options.scopes,
queryParams: options.queryParams,
flowType: options.flowType,
})
// try to open on the browser
if (isBrowser() && !options.skipBrowserRedirect) {
window.location.assign(url)
}

return { data: { provider, url }, error: null }
}

Expand Down Expand Up @@ -1355,13 +1392,15 @@ export default class GoTrueClient {
* @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
* @param options.scopes A space-separated list of scopes granted to the OAuth application.
* @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
* @param options.flowType OAuth flow to use - defaults to implicit flow. PKCE is recommended for mobile and server-side applications.
*/
private _getUrlForProvider(
private async _getUrlForProvider(
provider: Provider,
options: {
redirectTo?: string
scopes?: string
queryParams?: { [key: string]: string }
flowType: OAuthFlowType
}
) {
const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
Expand All @@ -1371,10 +1410,22 @@ export default class GoTrueClient {
if (options?.scopes) {
urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
}
if (options?.flowType === 'pkce') {
const codeVerifier = await generatePKCEVerifier()
await setItemAsync(this.storage, `${this.storageKey}-oauth-code-verifier`, codeVerifier)
const codeChallenge = await generatePKCEChallenge(codeVerifier)
const flowParams = new URLSearchParams({
flow_type: `${encodeURIComponent(options.flowType)}`,
code_challenge: `${encodeURIComponent(codeChallenge)}`,
code_challenge_method: `${encodeURIComponent('s256')}`,
})
urlParams.push(flowParams.toString())
}
if (options?.queryParams) {
const query = new URLSearchParams(options.queryParams)
urlParams.push(query.toString())
}

return `${this.url}/authorize?${urlParams.join('&')}`
}

Expand Down
41 changes: 40 additions & 1 deletion src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SupportedStorage } from './types'

import sha256CryptoJS from 'crypto-js/sha256'
export function expiresAt(expiresIn: number) {
const timeNow = Math.round(Date.now() / 1000)
return timeNow + expiresIn
Expand Down Expand Up @@ -236,3 +236,42 @@ export function retryable<T>(

return promise
}

function dec2hex(dec: number) {
return ('0' + dec.toString(16)).substr(-2)
}

// Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs
export async function generatePKCEVerifier() {
const verifierLength = 56
const array = new Uint32Array(verifierLength)
if (!isBrowser()) {
for (let i = 0; i < verifierLength; i++) {
array[i] = Math.floor(Math.random() * 256)
}
}
return Array.from(array, dec2hex).join('')
}

async function sha256(randomString: string) {
const encoder = new TextEncoder()
const encodedData = encoder.encode(randomString)
if (!isBrowser()) {
return sha256CryptoJS(randomString).toString()
}
const hash = await window.crypto.subtle.digest('SHA-256', encodedData)
const bytes = new Uint8Array(hash)

return Array.from(bytes)
.map((c) => String.fromCharCode(c))
.join('')
}

function base64urlencode(str: string) {
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

export async function generatePKCEChallenge(verifier: string) {
const hashed = await sha256(verifier)
return base64urlencode(hashed)
}
3 changes: 3 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ export type SignInWithPasswordlessCredentials =
}
}

export type OAuthFlowType = 'implicit' | 'pkce'
export type SignInWithOAuthCredentials = {
/** One of the providers supported by GoTrue. */
provider: Provider
Expand All @@ -441,6 +442,8 @@ export type SignInWithOAuthCredentials = {
queryParams?: { [key: string]: string }
/** If set to true does not immediately redirect the current browser context to visit the OAuth authorization page for the provider. */
skipBrowserRedirect?: boolean
/** If set to 'pkce' PKCE flow. Defaults to the 'implicit' flow otherwise */
flowType?: OAuthFlowType
}
}

Expand Down

0 comments on commit 2fc2781

Please sign in to comment.