-
-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds JWT validation for Apple and Google (#114)
* Adds JWKSCache and validation for apple and google * cleanup * Updated to know about must-revalidate, no-store and no-cache * Renamed hd to hostDomain * Updated jwt-kit dependency version * Also checks for must-revalidate on cache now * Algabreic simplification * argh * Updated to use EndpointCache * Removed a test as it's in vapor/vapor now * separate google + apple helpers * request * updates Co-authored-by: Tanner <[email protected]>
- Loading branch information
1 parent
0ecb6fa
commit 0f79b27
Showing
6 changed files
with
276 additions
and
154 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import Vapor | ||
|
||
extension Request.JWT { | ||
public var apple: Apple { | ||
.init(request: self.request) | ||
} | ||
|
||
public struct Apple { | ||
let request: Request | ||
|
||
public func verify(applicationIdentifier: String? = nil) -> EventLoopFuture<AppleIdentityToken> { | ||
guard let token = self.request.headers.bearerAuthorization?.token else { | ||
self.request.logger.error("Request is missing JWT bearer header.") | ||
return self.request.eventLoop.makeFailedFuture(Abort(.unauthorized)) | ||
} | ||
return self.verify(token, applicationIdentifier: applicationIdentifier) | ||
} | ||
|
||
public func verify(_ message: String, applicationIdentifier: String? = nil) -> EventLoopFuture<AppleIdentityToken> { | ||
self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) | ||
} | ||
|
||
public func verify<Message>(_ message: Message, applicationIdentifier: String? = nil) -> EventLoopFuture<AppleIdentityToken> | ||
where Message: DataProtocol | ||
{ | ||
self.request.application.jwt.apple.signers( | ||
on: self.request | ||
).flatMapThrowing { signers in | ||
let token = try signers.verify(message, as: AppleIdentityToken.self) | ||
if let applicationIdentifier = applicationIdentifier ?? self.request.application.jwt.apple.applicationIdentifier { | ||
guard token.audience.value == applicationIdentifier else { | ||
throw JWTError.claimVerificationFailure( | ||
name: "audience", | ||
reason: "Audience claim does not match application identifier" | ||
) | ||
} | ||
} | ||
return token | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension Application.JWT { | ||
public var apple: Apple { | ||
.init(jwt: self) | ||
} | ||
|
||
public struct Apple { | ||
let jwt: Application.JWT | ||
|
||
public func signers(on request: Request) -> EventLoopFuture<JWTSigners> { | ||
self.jwks.get(on: request).flatMapThrowing { | ||
let signers = JWTSigners() | ||
try signers.use(jwks: $0) | ||
return signers | ||
} | ||
} | ||
|
||
public var jwks: EndpointCache<JWKS> { | ||
self.storage.jwks | ||
} | ||
|
||
public var applicationIdentifier: String? { | ||
get { | ||
self.storage.applicationIdentifier | ||
} | ||
nonmutating set { | ||
self.storage.applicationIdentifier = newValue | ||
} | ||
} | ||
|
||
private struct Key: StorageKey, LockKey { | ||
typealias Value = Storage | ||
} | ||
|
||
private final class Storage { | ||
let jwks: EndpointCache<JWKS> | ||
var applicationIdentifier: String? | ||
init() { | ||
self.jwks = .init(uri: "https://appleid.apple.com/auth/keys") | ||
self.applicationIdentifier = nil | ||
} | ||
} | ||
|
||
private var storage: Storage { | ||
if let existing = self.jwt.application.storage[Key.self] { | ||
return existing | ||
} else { | ||
let lock = self.jwt.application.locks.lock(for: Key.self) | ||
lock.lock() | ||
defer { lock.unlock() } | ||
if let existing = self.jwt.application.storage[Key.self] { | ||
return existing | ||
} | ||
let new = Storage() | ||
self.jwt.application.storage[Key.self] = new | ||
return new | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import Vapor | ||
|
||
extension Request.JWT { | ||
public var google: Google { | ||
.init(request: self.request) | ||
} | ||
|
||
public struct Google { | ||
let request: Request | ||
|
||
public func verify( | ||
applicationIdentifier: String? = nil, | ||
gSuiteDomainName: String? = nil | ||
) -> EventLoopFuture<GoogleIdentityToken> { | ||
guard let token = self.request.headers.bearerAuthorization?.token else { | ||
self.request.logger.error("Request is missing JWT bearer header.") | ||
return self.request.eventLoop.makeFailedFuture(Abort(.unauthorized)) | ||
} | ||
return self.verify( | ||
token, | ||
applicationIdentifier: applicationIdentifier, | ||
gSuiteDomainName: gSuiteDomainName | ||
) | ||
} | ||
|
||
public func verify( | ||
_ message: String, | ||
applicationIdentifier: String? = nil, | ||
gSuiteDomainName: String? = nil | ||
) -> EventLoopFuture<GoogleIdentityToken> { | ||
self.verify( | ||
[UInt8](message.utf8), | ||
applicationIdentifier: applicationIdentifier, | ||
gSuiteDomainName: gSuiteDomainName | ||
) | ||
} | ||
|
||
public func verify<Message>( | ||
_ message: Message, | ||
applicationIdentifier: String? = nil, | ||
gSuiteDomainName: String? = nil | ||
) -> EventLoopFuture<GoogleIdentityToken> | ||
where Message: DataProtocol | ||
{ | ||
self.request.application.jwt.google.signers( | ||
on: self.request | ||
).flatMapThrowing { signers in | ||
let token = try signers.verify(message, as: GoogleIdentityToken.self) | ||
if let applicationIdentifier = applicationIdentifier ?? self.request.application.jwt.google.applicationIdentifier { | ||
guard token.audience.value == applicationIdentifier else { | ||
throw JWTError.claimVerificationFailure( | ||
name: "audience", | ||
reason: "Audience claim does not match application identifier" | ||
) | ||
} | ||
|
||
} | ||
if let gSuiteDomainName = gSuiteDomainName ?? self.request.application.jwt.google.gSuiteDomainName { | ||
guard let hd = token.hostedDomain, hd.value == gSuiteDomainName else { | ||
throw JWTError.claimVerificationFailure( | ||
name: "hostedDomain", | ||
reason: "Hosted domain claim does not match gSuite domain name" | ||
) | ||
} | ||
} | ||
return token | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
extension Application.JWT { | ||
public var google: Google { | ||
.init(jwt: self) | ||
} | ||
|
||
public struct Google { | ||
let jwt: Application.JWT | ||
|
||
public func signers(on request: Request) -> EventLoopFuture<JWTSigners> { | ||
self.jwks.get(on: request).flatMapThrowing { | ||
let signers = JWTSigners() | ||
try signers.use(jwks: $0) | ||
return signers | ||
} | ||
} | ||
|
||
public var jwks: EndpointCache<JWKS> { | ||
self.storage.jwks | ||
} | ||
|
||
public var applicationIdentifier: String? { | ||
get { | ||
self.storage.applicationIdentifier | ||
} | ||
nonmutating set { | ||
self.storage.applicationIdentifier = newValue | ||
} | ||
} | ||
|
||
public var gSuiteDomainName: String? { | ||
get { | ||
self.storage.gSuiteDomainName | ||
} | ||
nonmutating set { | ||
self.storage.gSuiteDomainName = newValue | ||
} | ||
} | ||
|
||
private struct Key: StorageKey, LockKey { | ||
typealias Value = Storage | ||
} | ||
|
||
private final class Storage { | ||
let jwks: EndpointCache<JWKS> | ||
var applicationIdentifier: String? | ||
var gSuiteDomainName: String? | ||
init() { | ||
self.jwks = .init(uri: "https://www.googleapis.com/oauth2/v3/certs") | ||
self.applicationIdentifier = nil | ||
self.gSuiteDomainName = nil | ||
} | ||
} | ||
|
||
private var storage: Storage { | ||
if let existing = self.jwt.application.storage[Key.self] { | ||
return existing | ||
} else { | ||
let lock = self.jwt.application.locks.lock(for: Key.self) | ||
lock.lock() | ||
defer { lock.unlock() } | ||
if let existing = self.jwt.application.storage[Key.self] { | ||
return existing | ||
} | ||
let new = Storage() | ||
self.jwt.application.storage[Key.self] = new | ||
return new | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.