Skip to content

Commit 3220b78

Browse files
authored
Merge pull request #2 from brokenhandsio/rfc7662-token-introspection
RFC 7662 Token Introspection
2 parents 482d493 + a2950c3 commit 3220b78

69 files changed

Lines changed: 1024 additions & 212 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ xcuserdata
1212
DerivedData/
1313
.DS_Store
1414
Package.pins
15+
Package.resolved
1516

1617
# End of https://www.gitignore.io/api/vapor

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import PackageDescription
22

33
let package = Package(
4-
name: "vapor-oauth",
4+
name: "VaporOAuth",
55
dependencies: [
66
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2),
77
.Package(url: "https://github.com/vapor/auth-provider.git", majorVersion: 1),

README.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Vapor OAuth is an OAuth2 Provider Library written for Vapor. You can integrate t
2020

2121
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.
2222

23+
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.
24+
2325
Vapor OAuth supports the standard grant types:
2426

2527
* Authorization Code
@@ -45,13 +47,13 @@ dependencies: [
4547
Next import the library into where you set up your `Droplet`:
4648

4749
```swift
48-
import OAuth
50+
import VaporOAuth
4951
```
5052

5153
Then add the provider to your `Config`:
5254

5355
```swift
54-
try addProvider(OAuth.Provider(codeManager: MyCodeManager(), tokenManager: MyTokenManager(), clientRetriever: MyClientRetriever(), authorizeHandler: MyAuthHandler(), userManager: MyUserManager(), validScopes: ["view_profile", "edit_profile"]))
56+
try addProvider(VaporOAuth.Provider(codeManager: MyCodeManager(), tokenManager: MyTokenManager(), clientRetriever: MyClientRetriever(), authorizeHandler: MyAuthHandler(), userManager: MyUserManager(), validScopes: ["view_profile", "edit_profile"], resourceServerRetriever: MyResourceServerRetriever()))
5557
```
5658

5759
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
6264
* `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
6365
* `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.
6466
* `validScopes` - this is an optional array of scopes that you wish to support in your system.
67+
* `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
6568

6669
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).
6770

@@ -79,6 +82,10 @@ This will throw a 401 error if the token is not valid or does not contain the `p
7982

8083
You can also get the user with `try request.oauth.user()`.
8184

85+
### Protecting Resource Servers With Remote Auth Server
86+
87+
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.
88+
8289
# Grant Types
8390

8491
## Authorization Code Grant
@@ -141,3 +148,44 @@ Note that if you are using the password flow, as per [the specification](https:/
141148
## Client Credentials Grant
142149

143150
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.
151+
152+
## Token Introspection
153+
154+
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`.
155+
156+
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:
157+
158+
```json
159+
{
160+
"active": true,
161+
"client_id": "ABDED0123456",
162+
"scope": "email profile",
163+
"exp": 1503445858,
164+
"user_id": "12345678",
165+
"username": "hansolo",
166+
"email_address": "hansolo@therebelalliance.com"
167+
}
168+
```
169+
170+
If the token has expired or does not exist then it will simply return:
171+
172+
```json
173+
{
174+
"active": false
175+
}
176+
```
177+
178+
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.
179+
180+
**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.
181+
182+
### Protecting Endpoints
183+
184+
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:
185+
186+
* `tokenIntrospectionEndpoint` - the endpoint where the token can be validated
187+
* `client` - the `Droplet`'s client to send the token validation request with
188+
* `resourceServerUsername` - the username of the resource server
189+
* `resourceServerPassword` - the password of the resource server
190+
191+
Once either of these has been set up, you can then call `request.oauth.user()` or `request.oauth.assertScopes()` like normal.

Sources/OAuth/Helper.swift

Lines changed: 0 additions & 92 deletions
This file was deleted.

Sources/OAuth/DefaultImplementations/EmptyAuthorizationHandler.swift renamed to Sources/VaporOAuth/DefaultImplementations/EmptyAuthorizationHandler.swift

File renamed without changes.

Sources/OAuth/DefaultImplementations/EmptyCodeManager.swift renamed to Sources/VaporOAuth/DefaultImplementations/EmptyCodeManager.swift

File renamed without changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public struct EmptyResourceServerRetriever: ResourceServerRetriever {
2+
3+
public init() {}
4+
5+
public func getServer(_ username: String) -> OAuthResourceServer? {
6+
return nil
7+
}
8+
}

Sources/OAuth/DefaultImplementations/EmptyUserManager.swift renamed to Sources/VaporOAuth/DefaultImplementations/EmptyUserManager.swift

File renamed without changes.

Sources/OAuth/DefaultImplementations/StaticClientRetriever.swift renamed to Sources/VaporOAuth/DefaultImplementations/StaticClientRetriever.swift

File renamed without changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import HTTP
2+
import Vapor
3+
4+
let oauthHelperKey = "oauth-helper"
5+
6+
public final class Helper {
7+
8+
public static func setup(for request: Request, tokenIntrospectionEndpoint: String, client: ClientFactoryProtocol,
9+
resourceServerUsername: String, resourceServerPassword: String) {
10+
let helper = Helper(request: request, tokenIntrospectionEndpoint: tokenIntrospectionEndpoint, client: client,
11+
resourceServerUsername: resourceServerUsername, resourceServerPassword: resourceServerPassword)
12+
request.storage[oauthHelperKey] = helper
13+
}
14+
15+
let oauthHelper: OAuthHelper
16+
17+
init(request: Request, provider: OAuth2Provider?) {
18+
self.oauthHelper = LocalOAuthHelper(request: request, tokenAuthenticator: provider?.tokenHandler.tokenAuthenticator,
19+
userManager: provider?.userManager, tokenManager: provider?.tokenManager)
20+
}
21+
22+
init(request: Request, tokenIntrospectionEndpoint: String, client: ClientFactoryProtocol,
23+
resourceServerUsername: String, resourceServerPassword: String) {
24+
self.oauthHelper = RemoteOAuthHelper(request: request, tokenIntrospectionEndpoint: tokenIntrospectionEndpoint,
25+
client: client, resourceServerUsername: resourceServerUsername,
26+
resourceServerPassword: resourceServerPassword)
27+
}
28+
29+
public func assertScopes(_ scopes: [String]?) throws {
30+
try oauthHelper.assertScopes(scopes)
31+
}
32+
33+
public func user() throws -> OAuthUser {
34+
return try oauthHelper.user()
35+
}
36+
}
37+
38+
extension Request {
39+
public var oauth: Helper {
40+
if let existing = storage[oauthHelperKey] as? Helper {
41+
return existing
42+
}
43+
44+
let helper = Helper(request: self, provider: Request.oauthProvider)
45+
storage[oauthHelperKey] = helper
46+
47+
return helper
48+
}
49+
50+
static var oauthProvider: OAuth2Provider?
51+
}
52+
53+
extension Request {
54+
func getOAuthToken() throws -> String {
55+
guard let authHeader = headers[.authorization] else {
56+
throw Abort(.forbidden)
57+
}
58+
59+
guard authHeader.lowercased().hasPrefix("bearer ") else {
60+
throw Abort(.forbidden)
61+
}
62+
63+
let token = authHeader.substring(from: authHeader.index(authHeader.startIndex, offsetBy: 7))
64+
65+
guard !token.isEmpty else {
66+
throw Abort(.forbidden)
67+
}
68+
69+
return token
70+
}
71+
}

0 commit comments

Comments
 (0)