-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from brokenhandsio/rfc7662-token-introspection
RFC 7662 Token Introspection
- Loading branch information
Showing
69 changed files
with
1,024 additions
and
212 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,5 +12,6 @@ xcuserdata | |
DerivedData/ | ||
.DS_Store | ||
Package.pins | ||
Package.resolved | ||
|
||
# End of https://www.gitignore.io/api/vapor |
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 |
---|---|---|
|
@@ -20,6 +20,8 @@ Vapor OAuth is an OAuth2 Provider Library written for Vapor. You can integrate t | |
|
||
It follows both [RFC 6749](https://tools.ietf.org/html/rfc6749) and [RFC6750](https://tools.ietf.org/html/rfc6749) and there is an extensive test suite to make sure it adheres to the specification. | ||
|
||
It also implements the [RFC 7662](https://tools.ietf.org/html/rfc7662) specification for Token Introspection, which is useful for microservices with a shared, central authorization server. | ||
|
||
Vapor OAuth supports the standard grant types: | ||
|
||
* Authorization Code | ||
|
@@ -45,13 +47,13 @@ dependencies: [ | |
Next import the library into where you set up your `Droplet`: | ||
|
||
```swift | ||
import OAuth | ||
import VaporOAuth | ||
``` | ||
|
||
Then add the provider to your `Config`: | ||
|
||
```swift | ||
try addProvider(OAuth.Provider(codeManager: MyCodeManager(), tokenManager: MyTokenManager(), clientRetriever: MyClientRetriever(), authorizeHandler: MyAuthHandler(), userManager: MyUserManager(), validScopes: ["view_profile", "edit_profile"])) | ||
try addProvider(VaporOAuth.Provider(codeManager: MyCodeManager(), tokenManager: MyTokenManager(), clientRetriever: MyClientRetriever(), authorizeHandler: MyAuthHandler(), userManager: MyUserManager(), validScopes: ["view_profile", "edit_profile"], resourceServerRetriever: MyResourceServerRetriever())) | ||
``` | ||
|
||
To integrate the library, you need to set up a number of things, which implement the various protocols required: | ||
|
@@ -62,6 +64,7 @@ To integrate the library, you need to set up a number of things, which implement | |
* `AuthorizeHandler` - this is responsible for allowing users to allow/deny authorization requests. See below for more details. If you do not want to support this grant type you can exclude this parameter and use the default implementation | ||
* `UserManager` - this is responsible for authenticating and getting users for the Password Credentials flow. If you do not want to support this flow, you can exclude this parameter and use the default implementation. | ||
* `validScopes` - this is an optional array of scopes that you wish to support in your system. | ||
* `ResourceServerRetriever` - this is only required if using the Token Introspection Endpoint and is what is used to authenticate resource servers trying to access the endpoint | ||
|
||
Note that there are a number of default implementations for the different required protocols for Fluent in the [Vapor OAuth Fluent package](https://github.com/brokenhandsio/vapor-oauth-fluent). | ||
|
||
|
@@ -79,6 +82,10 @@ This will throw a 401 error if the token is not valid or does not contain the `p | |
|
||
You can also get the user with `try request.oauth.user()`. | ||
|
||
### Protecting Resource Servers With Remote Auth Server | ||
|
||
If you have resource servers that are not the same server as the OAuth server that you wish to protect using the Token Introspection Endpoint, things are slightly different. See the [Token Introspection](#token-introspection) section for more information. | ||
|
||
# Grant Types | ||
|
||
## Authorization Code Grant | ||
|
@@ -141,3 +148,44 @@ Note that if you are using the password flow, as per [the specification](https:/ | |
## Client Credentials Grant | ||
|
||
Client Credentials is a userless flow and is designed for servers accessing other servers without the need for a user. Access is granted based upon the authentication of the client requesting access. | ||
|
||
## Token Introspection | ||
|
||
If running a microservices architecture it is useful to have a single server that handles authorization, which all the other resource servers query. To do this, you can use the Token Introspection Endpoint extension. In Vapor OAuth, this adds an endpoint you can post tokens tokens at `/oauth/token_info`. | ||
|
||
You can send a POST request to this endpoint with a single parameter, `token`, which contains the OAuth token you want to check. If it is valid and active, then it will return a JSON payload, that looks similar to: | ||
|
||
```json | ||
{ | ||
"active": true, | ||
"client_id": "ABDED0123456", | ||
"scope": "email profile", | ||
"exp": 1503445858, | ||
"user_id": "12345678", | ||
"username": "hansolo", | ||
"email_address": "[email protected]" | ||
} | ||
``` | ||
|
||
If the token has expired or does not exist then it will simply return: | ||
|
||
```json | ||
{ | ||
"active": false | ||
} | ||
``` | ||
|
||
This endpoint is protected using HTTP Basic Authentication so you need to send an `Authorization: Basic abc` header with the request. This will check the `ResourceServerRetriever` for the username and password sent. | ||
|
||
**Note:** as per [the spec](https://tools.ietf.org/html/rfc7662#section-4) - the token introspection endpoint MUST be protected by HTTPS - this means the server must be behind a TLS certificate (commonly known as SSL). Vapor OAuth leaves this up to the integrating library to implement. | ||
|
||
### Protecting Endpoints | ||
|
||
To protect resources on other servers with OAuth using the Token Introspection endpoint, you either need to use the `OAuth2TokenIntrospectionMiddleware` on your routes that you want to protect, or you need to manually set up the `Helper` object (the middleware does this for you). Both the middleware and helper setup require: | ||
|
||
* `tokenIntrospectionEndpoint` - the endpoint where the token can be validated | ||
* `client` - the `Droplet`'s client to send the token validation request with | ||
* `resourceServerUsername` - the username of the resource server | ||
* `resourceServerPassword` - the password of the resource server | ||
|
||
Once either of these has been set up, you can then call `request.oauth.user()` or `request.oauth.assertScopes()` like normal. |
This file was deleted.
Oops, something went wrong.
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions
8
Sources/VaporOAuth/DefaultImplementations/EmptyResourceServerRetriever.swift
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,8 @@ | ||
public struct EmptyResourceServerRetriever: ResourceServerRetriever { | ||
|
||
public init() {} | ||
|
||
public func getServer(_ username: String) -> OAuthResourceServer? { | ||
return nil | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
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,71 @@ | ||
import HTTP | ||
import Vapor | ||
|
||
let oauthHelperKey = "oauth-helper" | ||
|
||
public final class Helper { | ||
|
||
public static func setup(for request: Request, tokenIntrospectionEndpoint: String, client: ClientFactoryProtocol, | ||
resourceServerUsername: String, resourceServerPassword: String) { | ||
let helper = Helper(request: request, tokenIntrospectionEndpoint: tokenIntrospectionEndpoint, client: client, | ||
resourceServerUsername: resourceServerUsername, resourceServerPassword: resourceServerPassword) | ||
request.storage[oauthHelperKey] = helper | ||
} | ||
|
||
let oauthHelper: OAuthHelper | ||
|
||
init(request: Request, provider: OAuth2Provider?) { | ||
self.oauthHelper = LocalOAuthHelper(request: request, tokenAuthenticator: provider?.tokenHandler.tokenAuthenticator, | ||
userManager: provider?.userManager, tokenManager: provider?.tokenManager) | ||
} | ||
|
||
init(request: Request, tokenIntrospectionEndpoint: String, client: ClientFactoryProtocol, | ||
resourceServerUsername: String, resourceServerPassword: String) { | ||
self.oauthHelper = RemoteOAuthHelper(request: request, tokenIntrospectionEndpoint: tokenIntrospectionEndpoint, | ||
client: client, resourceServerUsername: resourceServerUsername, | ||
resourceServerPassword: resourceServerPassword) | ||
} | ||
|
||
public func assertScopes(_ scopes: [String]?) throws { | ||
try oauthHelper.assertScopes(scopes) | ||
} | ||
|
||
public func user() throws -> OAuthUser { | ||
return try oauthHelper.user() | ||
} | ||
} | ||
|
||
extension Request { | ||
public var oauth: Helper { | ||
if let existing = storage[oauthHelperKey] as? Helper { | ||
return existing | ||
} | ||
|
||
let helper = Helper(request: self, provider: Request.oauthProvider) | ||
storage[oauthHelperKey] = helper | ||
|
||
return helper | ||
} | ||
|
||
static var oauthProvider: OAuth2Provider? | ||
} | ||
|
||
extension Request { | ||
func getOAuthToken() throws -> String { | ||
guard let authHeader = headers[.authorization] else { | ||
throw Abort(.forbidden) | ||
} | ||
|
||
guard authHeader.lowercased().hasPrefix("bearer ") else { | ||
throw Abort(.forbidden) | ||
} | ||
|
||
let token = authHeader.substring(from: authHeader.index(authHeader.startIndex, offsetBy: 7)) | ||
|
||
guard !token.isEmpty else { | ||
throw Abort(.forbidden) | ||
} | ||
|
||
return token | ||
} | ||
} |
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,55 @@ | ||
import Vapor | ||
|
||
struct LocalOAuthHelper: OAuthHelper { | ||
|
||
weak var request: Request? | ||
let tokenAuthenticator: TokenAuthenticator? | ||
let userManager: UserManager? | ||
let tokenManager: TokenManager? | ||
|
||
func assertScopes(_ scopes: [String]?) throws { | ||
guard let tokenAuthenticator = tokenAuthenticator else { | ||
throw Abort(.forbidden) | ||
} | ||
|
||
let accessToken = try getToken() | ||
|
||
guard tokenAuthenticator.validateAccessToken(accessToken, requiredScopes: scopes) else { | ||
throw Abort.unauthorized | ||
} | ||
} | ||
|
||
func user() throws -> OAuthUser { | ||
guard let userManager = userManager else { | ||
throw Abort(.forbidden) | ||
} | ||
|
||
let token = try getToken() | ||
|
||
guard let userID = token.userID else { | ||
throw Abort.unauthorized | ||
} | ||
|
||
guard let user = userManager.getUser(userID: userID) else { | ||
throw Abort.unauthorized | ||
} | ||
|
||
return user | ||
} | ||
|
||
private func getToken() throws -> AccessToken { | ||
guard let tokenManager = tokenManager, let token = try request?.getOAuthToken() else { | ||
throw Abort(.forbidden) | ||
} | ||
|
||
guard let accessToken = tokenManager.getAccessToken(token) else { | ||
throw Abort.unauthorized | ||
} | ||
|
||
guard accessToken.expiryTime >= Date() else { | ||
throw Abort.unauthorized | ||
} | ||
|
||
return accessToken | ||
} | ||
} |
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,4 @@ | ||
protocol OAuthHelper { | ||
func assertScopes(_ scopes: [String]?) throws | ||
func user() throws -> OAuthUser | ||
} |
Oops, something went wrong.