Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth 2.0 Device Authorization Grant #6

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 54 additions & 36 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client.git",
"state" : {
"revision" : "fc510a39cff61b849bf5cdff17eb2bd6d0777b49",
"version" : "1.11.5"
"revision" : "0a9b72369b9d87ab155ef585ef50700a34abf070",
"version" : "1.23.1"
}
},
{
"identity" : "async-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/async-kit.git",
"state" : {
"revision" : "c3329e444bafbb12d1d312af9191be95348a8175",
"version" : "1.13.0"
"revision" : "eab9edff78e8ace20bd7cb6e792ab46d54f59ab9",
"version" : "1.18.0"
}
},
{
"identity" : "console-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/console-kit.git",
"state" : {
"revision" : "a7e67a1719933318b5ab7eaaed355cde020465b1",
"version" : "4.5.0"
"revision" : "966d89ae64cd71c652a1e981bc971de59d64f13d",
"version" : "4.15.1"
}
},
{
Expand All @@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/routing-kit.git",
"state" : {
"revision" : "ffac7b3a127ce1e85fb232f1a6271164628809ad",
"version" : "4.6.0"
"revision" : "8c9a227476555c55837e569be71944e02a056b72",
"version" : "4.9.1"
}
},
{
Expand All @@ -55,21 +55,21 @@
}
},
{
"identity" : "swift-atomics",
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe",
"version" : "1.0.2"
"revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6",
"version" : "1.3.0"
}
},
{
"identity" : "swift-backtrace",
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/swift-backtrace.git",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "f25620d5d05e2f1ba27154b40cafea2b67566956",
"version" : "1.3.3"
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
Expand All @@ -86,71 +86,80 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "d9825fa541df64b1a7b182178d61b9a82730d01f",
"version" : "2.1.0"
"revision" : "21f7878f2b39d46fd8ba2b06459ccb431cdf876c",
"version" : "3.8.1"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types",
"state" : {
"revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd",
"version" : "1.3.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c",
"version" : "1.4.4"
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.6.1"
}
},
{
"identity" : "swift-metrics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-metrics.git",
"state" : {
"revision" : "53be78637ecd165d1ddedc4e20de69b8f43ec3b7",
"version" : "2.3.2"
"revision" : "e0165b53d49b413dd987526b641e05e246782685",
"version" : "2.5.0"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "b4e0a274f7f34210e97e2f2c50ab02a10b549250",
"version" : "2.41.1"
"revision" : "914081701062b11e3bb9e21accc379822621995e",
"version" : "2.76.1"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "6c84d247754ad77487a6f0694273b89b83efd056",
"version" : "1.14.0"
"revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6",
"version" : "1.24.1"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "f9ab1c94c80d568efd762d2a638f25162691d766",
"version" : "1.22.1"
"revision" : "eaa71bb6ae082eee5a07407b1ad0cbd8f48f9dca",
"version" : "1.34.1"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "ba7c0d7f82affc518147ea61d240330bf7f7ea9b",
"version" : "2.22.1"
"revision" : "d7ceaf0e4d8001cd35cdc12e42cdd281e9e564e8",
"version" : "2.28.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "4e02d9cf35cabfb538c96613272fb027dd0c8692",
"version" : "1.13.1"
"revision" : "bbd5e63cf949b7db0c9edaf7a21e141c52afe214",
"version" : "1.23.0"
}
},
{
Expand All @@ -162,22 +171,31 @@
"version" : "1.0.2"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "c8a44d836fe7913603e246acab7c528c2e780168",
"version" : "1.4.0"
}
},
{
"identity" : "vapor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/vapor.git",
"state" : {
"revision" : "dda0de537e7906414dccd551e77095be1e34e3da",
"version" : "4.65.2"
"revision" : "4d3bc6ce08b72a14c9879810cf0be455ca98f1fb",
"version" : "4.106.1"
}
},
{
"identity" : "websocket-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/websocket-kit.git",
"state" : {
"revision" : "2d9d2188a08eef4a869d368daab21b3c08510991",
"version" : "2.6.1"
"revision" : "4232d34efa49f633ba61afde365d3896fc7f8740",
"version" : "2.15.0"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

public struct EmptyDeviceCodeManager: DeviceCodeManager {
public func updateLastPolled(_ deviceCode: String) async throws {
return
}

public func increaseInterval(_ deviceCode: String, by seconds: Int) async throws {
return
}


public func authorizeDeviceCode(_ deviceCode: OAuthDeviceCode, userID: String) async throws {
return
}

public func removeDeviceCode(_ deviceCode: OAuthDeviceCode) async throws {
return
}

public init() {}

public func generateDeviceCode(
clientID: String,
scopes: [String]?,
verificationURI: String,
verificationURIComplete: String?
) async throws -> OAuthDeviceCode? {
return nil
}

public func getDeviceCode(_ code: String) async throws -> OAuthDeviceCode? {
return nil
}

public func getUserCode(_ code: String) async throws -> OAuthDeviceCode? {
return nil
}
}
53 changes: 53 additions & 0 deletions Sources/VaporOAuth/Models/OAuthDeviceCode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

public final class OAuthDeviceCode: @unchecked Sendable {
public let deviceCode: String
public let userCode: String
public let clientID: String
public let verificationURI: String
public let verificationURIComplete: String?
public let expiryDate: Date
public let interval: Int
public let scopes: [String]?
public var status: DeviceCodeStatus
public var userID: String?
public let lastPolled: Date?

public var shouldIncreasePollInterval: Bool {
guard let lastPolled = lastPolled else { return false }
return Date().timeIntervalSince(lastPolled) < Double(interval)
}

public init(
deviceCode: String,
userCode: String,
clientID: String,
verificationURI: String,
verificationURIComplete: String?,
expiryDate: Date,
interval: Int,
scopes: [String]?,
status: DeviceCodeStatus = .pending,
userID: String? = nil,
lastPolled: Date? = nil
) {
self.deviceCode = deviceCode
self.userCode = userCode
self.clientID = clientID
self.verificationURI = verificationURI
self.verificationURIComplete = verificationURIComplete
self.expiryDate = expiryDate
self.interval = interval
self.scopes = scopes
self.status = status
self.userID = userID
self.lastPolled = lastPolled
}
}

public enum DeviceCodeStatus {
case pending
case authorized
case unauthorized
case declined
}
14 changes: 14 additions & 0 deletions Sources/VaporOAuth/OAuth2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Vapor
public struct OAuth2: LifecycleHandler {
let codeManager: CodeManager
let tokenManager: TokenManager
let deviceCodeManager: DeviceCodeManager
let clientRetriever: ClientRetriever
let authorizeHandler: AuthorizeHandler
let userManager: UserManager
Expand All @@ -13,6 +14,7 @@ public struct OAuth2: LifecycleHandler {
public init(
codeManager: CodeManager = EmptyCodeManager(),
tokenManager: TokenManager,
deviceCodeManager: DeviceCodeManager = EmptyDeviceCodeManager(),
clientRetriever: ClientRetriever,
authorizeHandler: AuthorizeHandler = EmptyAuthorizationHandler(),
userManager: UserManager = EmptyUserManager(),
Expand All @@ -24,6 +26,7 @@ public struct OAuth2: LifecycleHandler {
self.clientRetriever = clientRetriever
self.authorizeHandler = authorizeHandler
self.tokenManager = tokenManager
self.deviceCodeManager = deviceCodeManager
self.userManager = userManager
self.validScopes = validScopes
self.resourceServerRetriever = resourceServerRetriever
Expand All @@ -37,6 +40,7 @@ public struct OAuth2: LifecycleHandler {

private func addRoutes(to app: Application) {
let scopeValidator = ScopeValidator(validScopes: validScopes, clientRetriever: clientRetriever)

let clientValidator = ClientValidator(
clientRetriever: clientRetriever,
scopeValidator: scopeValidator,
Expand All @@ -48,6 +52,7 @@ public struct OAuth2: LifecycleHandler {
tokenManager: tokenManager,
scopeValidator: scopeValidator,
codeManager: codeManager,
deviceCodeManager: deviceCodeManager,
userManager: userManager,
logger: app.logger
)
Expand All @@ -67,13 +72,22 @@ public struct OAuth2: LifecycleHandler {
codeManager: codeManager,
clientValidator: clientValidator
)

let deviceAuthorizationHandler = DeviceAuthorizationHandler(
deviceCodeManager: deviceCodeManager,
clientValidator: clientValidator,
scopeValidator: scopeValidator
)

let resourceServerAuthenticator = ResourceServerAuthenticator(resourceServerRetriever: resourceServerRetriever)

// returning something like "Authenticate with GitHub page"
app.get("oauth", "authorize", use: authorizeGetHandler.handleRequest)
// pressing something like "Allow/Deny Access" button on "Authenticate with GitHub page". Returns a code.
app.grouped(OAuthUser.guardMiddleware()).post("oauth", "authorize", use: authorizePostHandler.handleRequest)

app.post("oauth", "device_authorization", use: deviceAuthorizationHandler.handleRequest)

// client requesting access/refresh token with code from POST /authorize endpoint
app.post("oauth", "token", use: tokenHandler.handleRequest)

Expand Down
27 changes: 27 additions & 0 deletions Sources/VaporOAuth/OAuthConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Vapor

public struct OAuthConfiguration: Sendable {
public let deviceVerificationURI: String

public init(deviceVerificationURI: String) {
self.deviceVerificationURI = deviceVerificationURI
}
}

extension Application {
private struct OAuthConfigurationKey: StorageKey {
typealias Value = OAuthConfiguration
}

public var oauth: OAuthConfiguration {
get {
guard let config = storage[OAuthConfigurationKey.self] else {
fatalError("OAuth configuration not set. Use app.oauth = OAuthConfiguration(...)")
}
return config
}
set {
storage[OAuthConfigurationKey.self] = newValue
}
}
}
Loading
Loading