Skip to content

Commit

Permalink
Merge pull request #6 from dewonderstruck/feat/device-code-flow
Browse files Browse the repository at this point in the history
OAuth 2.0 Device Authorization Grant
  • Loading branch information
vamsii777 authored Oct 28, 2024
2 parents a029889 + 04c9b04 commit 27e1d00
Show file tree
Hide file tree
Showing 14 changed files with 1,197 additions and 73 deletions.
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

0 comments on commit 27e1d00

Please sign in to comment.