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

getCurrentUser given both decoded & access tokens #779

Merged
merged 22 commits into from
Jul 9, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
042cd8e
getCurrentUser given both decoded & access tokens
dthyresson Jun 30, 2020
5b6a638
Corrects default case in decodeAuthToken
dthyresson Jun 30, 2020
8d71532
Adds params to generated auth.js getCurrentUser
dthyresson Jun 30, 2020
2affd4d
Refactor w/ AuthDecoder interface to decode token
dthyresson Jul 3, 2020
e337806
Netlify & GoTrue decoders missing NewClientContext
dthyresson Jul 3, 2020
5a98b7d
Changes requested for AuthorizationHeader plus ++
dthyresson Jul 3, 2020
171852b
Implemented getAuthenticationContext
dthyresson Jul 6, 2020
0b6acac
Get to green w/ fixed getAuthenticationContext
dthyresson Jul 6, 2020
64b48b9
Clean up getAuthenticationContext fall through
dthyresson Jul 6, 2020
e7ac26c
Implemented code style changes per PR review.
dthyresson Jul 7, 2020
e88cc88
Updates cli generate auth for new currentUser
dthyresson Jul 7, 2020
13f21de
Restores redirect_uri on auth0 provider generator
dthyresson Jul 8, 2020
bb1e5b5
Refactor auth implementation.
peterp Jul 9, 2020
2be2b47
Added warning about auth0 localstorage.
peterp Jul 9, 2020
ff23e33
Resolve graphql path.
peterp Jul 9, 2020
14bb24f
Reorder auth generator steps.
peterp Jul 9, 2020
bc56397
Do not install auth0 package by default.
peterp Jul 9, 2020
7934823
Revert template modifications.
peterp Jul 9, 2020
b078051
Fix AuthContextPayload.
peterp Jul 9, 2020
af4827c
Update docs.
peterp Jul 9, 2020
2caf290
Update packages/cli/src/commands/generate/auth/templates/firebase.aut…
peterp Jul 9, 2020
c519405
Merge branch 'main' into dt-auth-access-token
peterp Jul 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/api/src/auth/authDecoders/auth0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AuthenticationError } from 'apollo-server-lambda'
import { verifyAuth0Token } from 'src/auth/verifyAuth0Token'
import { getAuthorization } from 'src/auth/authHeaders'

import type { AuthDecoder } from './'
export type AuthDecoderAuth0 = AuthDecoder

export const decode = async ({
event,
}: {
event: APIGatewayProxyEvent
}): Promise<AuthToken> => {
try {
const authorization = getAuthorization(event)
const token = authorization['token']
const decoded = await verifyAuth0Token(token)
dthyresson marked this conversation as resolved.
Show resolved Hide resolved

return decoded
} catch {
throw new AuthenticationError(
'The authentication token could not be decoded.'
)
}
}

export const auth0 = (): AuthDecoderAuth0 => {
return {
type: 'auth0',
decodeToken: async ({ event }) => {
return decode({ event })
},
}
}
26 changes: 26 additions & 0 deletions packages/api/src/auth/authDecoders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { SupportedAuthTypes } from '@redwoodjs/auth'

import { netlify } from './netlify'
import { auth0 } from './auth0'
import { token } from './token'

const typesToDecoders = {
netlify: netlify,
auth0: auth0,
goTrue: netlify,
magicLink: token,
firebase: token,
/** Don't we support your auth client? Use the auth token in your own the `custom` client! */
custom: token,
}

export type AuthToken = null | object | string

export interface AuthDecoder {
decodeToken({ type, event, context }): Promise<AuthToken>
type: SupportedAuthTypes
}

export const createAuthDecoder = (type: SupportedAuthTypes): AuthDecoder => {
return typesToDecoders[type]()
}
55 changes: 55 additions & 0 deletions packages/api/src/auth/authDecoders/netlify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type {
APIGatewayProxyEvent,
Context as LambdaContext,
ClientContext,
} from 'aws-lambda'
import { AuthenticationError } from 'apollo-server-lambda'
import jwt from 'jsonwebtoken'
import { getAuthorization } from 'src/auth/authHeaders'

import type { AuthDecoder } from './'
export type AuthDecoderNetlify = AuthDecoder

type NewClientContext = ClientContext & {
user?: object
}

export const decode = async ({
event,
context,
}: {
event: APIGatewayProxyEvent
context: LambdaContext
}): Promise<AuthToken> => {
try {
let decoded: AuthToken = null

// Netlify verifies and decodes a JWT before the request hits the Serverless
// function so the decoded JWT is already available in production
if (process.env.NODE_ENV === 'production') {
const clientContext = context.clientContext as NewClientContext
decoded = clientContext?.user || null
} else {
// We emulate the native Netlify experience in development mode.
// We just decode it since we don't have the signing key.
const authorization = getAuthorization(event)
const token = authorization['token']
decoded = jwt.decode(token)
}

return decoded
} catch {
throw new AuthenticationError(
'The authentication token could not be decoded.'
)
}
}

export const netlify = (): AuthDecoderNetlify => {
return {
type: 'netlify',
decodeToken: async ({ event, context }) => {
return decode({ event, context })
},
}
}
33 changes: 33 additions & 0 deletions packages/api/src/auth/authDecoders/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AuthenticationError } from 'apollo-server-lambda'
import { getAuthorization } from 'src/auth/authHeaders'

import type { AuthDecoder } from './'
export type AuthDecoderToken = AuthDecoder

export const decode = async ({
event,
}: {
event: APIGatewayProxyEvent
context: LambdaContext
}): Promise<AuthToken> => {
try {
let decoded: AuthToken = null
const authorization = getAuthorization(event)
decoded = authorization['token']

return decoded
} catch {
throw new AuthenticationError(
'The authentication token could not be decoded.'
)
}
}

export const token = (): AuthDecoderToken => {
return {
type: 'token',
decodeToken: async ({ event }) => {
return decode({ event })
},
}
}
116 changes: 60 additions & 56 deletions packages/api/src/auth/authHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import type {
APIGatewayProxyEvent,
Context as LambdaContext,
ClientContext,
} from 'aws-lambda'
import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'
import type { SupportedAuthTypes } from '@redwoodjs/auth'
//
import jwt from 'jsonwebtoken'
import { AuthenticationError } from 'apollo-server-lambda'

import { verifyAuth0Token } from './verifyAuth0Token'
import type { AuthToken } from './authDecoders'
import { createAuthDecoder } from './authDecoders'

// This is shared by `@redwoodjs/web`
const AUTH_PROVIDER_HEADER = 'auth-provider'
Expand All @@ -19,11 +13,40 @@ export const getAuthProviderType = (
return event?.headers[AUTH_PROVIDER_HEADER] as SupportedAuthTypes
}

type NewClientContext = ClientContext & {
user?: object
export interface AuthorizationHeader {
schema: 'Bearer' | 'Basic'
token: string
}

export type AuthToken = null | object | string
/**
* This function returns the Authorization Header Schema and Token
* from the client request event headers in AuthorizationHeader
* to be decoded or otherwise used in token-based authentication
* when required to allow an application to access an API.
*
* @returns AuthorizationHeader
*/
export const getAuthorization = (event: {
event: APIGatewayProxyEvent
}): AuthorizationHeader => {
try {
const authorizationHeader: AuthorizationHeader = {
schema: null,
token: null,
}
const mechanism = event.headers?.authorization?.split(' ')
authorizationHeader.schema = mechanism?.[0]
authorizationHeader.token = mechanism?.[1]
dthyresson marked this conversation as resolved.
Show resolved Hide resolved

if (!mechanism.length === 2 || authorizationHeader.token.length === 0) {
throw new Error('Empty authorization token')
}
dthyresson marked this conversation as resolved.
Show resolved Hide resolved

return authorizationHeader
dthyresson marked this conversation as resolved.
Show resolved Hide resolved
} catch {
throw new Error('Error getting Authorization header')
}
}

/**
* Redwood supports multiple authentication providers. We add headers to the client
Expand All @@ -45,54 +68,35 @@ export const decodeAuthToken = async ({
event: APIGatewayProxyEvent
context: LambdaContext
}): Promise<AuthToken> => {
const token = event.headers?.authorization?.split(' ')?.[1]
if (!token && token.length === 0) {
throw new Error('Empty authorization token')
}
const authDecoder = createAuthDecoder(type)
const decoded = await authDecoder.decodeToken({ type, event, context })

let decoded: AuthToken = null
switch (type) {
case 'goTrue':
case 'netlify': {
// Netlify verifies and decodes a JWT before the request hits the Serverless
// function so the decoded jwt is already available in production
if (process.env.NODE_ENV === 'production') {
const clientContext = context.clientContext as NewClientContext
decoded = clientContext?.user || null
} else {
// We emualate the native Netlify experience in development mode.
// We just decode it since we don't have the signing key.
decoded = jwt.decode(token)
}
break
}
case 'auth0': {
decoded = await verifyAuth0Token(token)
break
}
return decoded
}

case 'firebase':
case 'magicLink': {
decoded = token
break
}
/**
* Get the authorization information from the request headers and request context.
* @returns [decoded, { type, token }]
**/
export const getAuthenticationContext = async ({
event,
context,
}: {
event: APIGatewayProxyEvent
context: GlobalContext & LambdaContext
}) => {
const type = getAuthProviderType(event)

// These tokens require a 3rd party library for decoding that we don't want to
// bundle with each installation. We'll cover it in the documentation.
default: {
decoded = {
type,
token,
}
break
}
}
let decoded = null
let token = null

if (decoded === null) {
throw new AuthenticationError(
'The authentication token could not be decoded.'
)
// if type is undefined, then not using an auth provider
dthyresson marked this conversation as resolved.
Show resolved Hide resolved
// ie, not "logged in"
if (typeof type !== 'undefined') {
decoded = await decodeAuthToken({ type, event, context })
const authorization = getAuthorization(event)
token = authorization['token']
}

return decoded
return [decoded, { type, token }]
}
17 changes: 11 additions & 6 deletions packages/api/src/functions/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { AuthToken } from 'src/auth/authHeaders'
import type { GlobalContext } from 'src/globalContext'
//
import { ApolloServer } from 'apollo-server-lambda'
import { getAuthProviderType, decodeAuthToken } from 'src/auth/authHeaders'
import { getAuthenticationContext } from 'src/auth/authHeaders'
import { setContext } from 'src/globalContext'

export type GetCurrentUser = (
Expand Down Expand Up @@ -37,14 +37,19 @@ export const createContextHandler = (
// such as database connections, to be released before returning a reponse.
context.callbackWaitsForEmptyEventLoop = false

// Get the authorization information from the request headers and request context.
const type = getAuthProviderType(event)
const [decoded, { type, token }] = await getAuthenticationContext({
event,
context,
})

if (typeof type !== 'undefined') {
const authToken = await decodeAuthToken({ type, event, context })
context.currentUser =
typeof getCurrentUser == 'function'
? await getCurrentUser(authToken)
: authToken
? await getCurrentUser(decoded, {
token,
authType: type,
dthyresson marked this conversation as resolved.
Show resolved Hide resolved
})
: decoded
}

// Sets the **global** context object, which can be imported with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import { AuthenticationError } from '@redwoodjs/api'

export const getCurrentUser = async (jwt) => {
return jwt
export const getCurrentUser = async (decoded, { token, authType }) => {
return decoded
}

// Use this function in your services to check that a user is logged in, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const config = {

const adminApp = admin.initializeApp(config)

export const getCurrentUser = async (token) => {
export const getCurrentUser = async (decoded, { token, authType }) => {
peterp marked this conversation as resolved.
Show resolved Hide resolved
peterp marked this conversation as resolved.
Show resolved Hide resolved
const { email, uid } = await adminApp.auth().verifyIdToken(token)
return { email, uid }
}
Expand Down