Skip to content

Commit

Permalink
Merge pull request #2 from brokenhandsio/rfc7662-token-introspection
Browse files Browse the repository at this point in the history
RFC 7662 Token Introspection
  • Loading branch information
0xTim authored Sep 25, 2017
2 parents 482d493 + a2950c3 commit 3220b78
Show file tree
Hide file tree
Showing 69 changed files with 1,024 additions and 212 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ xcuserdata
DerivedData/
.DS_Store
Package.pins
Package.resolved

# End of https://www.gitignore.io/api/vapor
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import PackageDescription

let package = Package(
name: "vapor-oauth",
name: "VaporOAuth",
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2),
.Package(url: "https://github.com/vapor/auth-provider.git", majorVersion: 1),
Expand Down
52 changes: 50 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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).

Expand All @@ -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
Expand Down Expand Up @@ -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.
92 changes: 0 additions & 92 deletions Sources/OAuth/Helper.swift

This file was deleted.

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
}
}
71 changes: 71 additions & 0 deletions Sources/VaporOAuth/Helper/Helper.swift
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
}
}
55 changes: 55 additions & 0 deletions Sources/VaporOAuth/Helper/LocalOAuthHelper.swift
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
}
}
4 changes: 4 additions & 0 deletions Sources/VaporOAuth/Helper/OAuthHelper.swift
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
}
Loading

0 comments on commit 3220b78

Please sign in to comment.