/
createJWTAuthScheme.js
141 lines (124 loc) · 4.34 KB
/
createJWTAuthScheme.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import Boom from '@hapi/boom'
import jwt from 'jsonwebtoken'
import serverlessLog from '../../serverlessLog.js'
const { isArray } = Array
export default function createAuthScheme(jwtOptions, { log }) {
const authorizerName = jwtOptions.name
const identitySourceMatch = /^\$request.header.((?:\w+-?)+\w+)$/.exec(
jwtOptions.identitySource,
)
if (!identitySourceMatch || identitySourceMatch.length !== 2) {
throw new Error(
`Serverless Offline only supports retrieving JWT from the headers (${authorizerName})`,
)
}
const identityHeader = identitySourceMatch[1].toLowerCase()
// Create Auth Scheme
return () => ({
async authenticate(request, h) {
if (log) {
log.notice()
log.notice(
`Running JWT Authorization function for ${request.method} ${request.path} (${authorizerName})`,
)
} else {
console.log('') // Just to make things a little pretty
// TODO: this only validates specific properties of the JWT
// it does not verify the JWT is correctly signed. That would
// be a great feature to add under an optional flag :)
serverlessLog(
`Running JWT Authorization function for ${request.method} ${request.path} (${authorizerName})`,
)
}
// Get Authorization header
const { req } = request.raw
let jwtToken = req.headers[identityHeader]
if (jwtToken && jwtToken.split(' ')[0] === 'Bearer') {
;[, jwtToken] = jwtToken.split(' ')
}
try {
const decoded = jwt.decode(jwtToken, { complete: true })
if (!decoded) {
return Boom.unauthorized('JWT not decoded')
}
const expirationDate = new Date(decoded.payload.exp * 1000)
if (expirationDate.valueOf() < Date.now()) {
return Boom.unauthorized('JWT Token expired')
}
const { iss, aud, scope } = decoded.payload
const clientId = decoded.payload.client_id
if (iss !== jwtOptions.issuerUrl) {
if (log) {
log.notice(`JWT Token not from correct issuer url`)
} else {
serverlessLog(`JWT Token not from correct issuer url`)
}
return Boom.unauthorized('JWT Token not from correct issuer url')
}
const validAudiences = isArray(jwtOptions.audience)
? jwtOptions.audience
: [jwtOptions.audience]
const providedAudiences = isArray(aud) ? aud : [aud]
const validAudienceProvided = providedAudiences.some((a) =>
validAudiences.includes(a),
)
if (!validAudienceProvided && !validAudiences.includes(clientId)) {
if (log) {
log.notice(`JWT Token does not contain correct audience`)
} else {
serverlessLog(`JWT Token does not contain correct audience`)
}
return Boom.unauthorized(
'JWT Token does not contain correct audience',
)
}
let scopes = null
if (jwtOptions.scopes && jwtOptions.scopes.length) {
if (!scope) {
if (log) {
log.notice(`JWT Token missing valid scope`)
} else {
serverlessLog(`JWT Token missing valid scope`)
}
return Boom.forbidden('JWT Token missing valid scope')
}
scopes = scope.split(' ')
if (
scopes.every((s) => {
return !jwtOptions.scopes.includes(s)
})
) {
if (log) {
log.notice(`JWT Token missing valid scope`)
} else {
serverlessLog(`JWT Token missing valid scope`)
}
return Boom.forbidden('JWT Token missing valid scope')
}
}
if (log) {
log.notice(`JWT Token validated`)
} else {
serverlessLog(`JWT Token validated`)
}
// Set the credentials for the rest of the pipeline
// return resolve(
return h.authenticated({
credentials: {
claims: decoded.payload,
scopes,
},
})
} catch (err) {
if (log) {
log.notice(`JWT could not be decoded`)
log.error(err)
} else {
serverlessLog(`JWT could not be decoded`)
serverlessLog(err)
}
return Boom.unauthorized('Unauthorized')
}
},
})
}