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

Handling Expired Device Codes in OAuth Token Process #27

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
10 changes: 10 additions & 0 deletions Sources/VaporOAuth/DefaultImplementations/EmptyCodeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ public struct EmptyCodeManager: CodeManager {
}

public func codeUsed(_ code: OAuthCode) {}

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

public func generateDeviceCode(userID: String, clientID: String, scopes: [String]?) throws -> String {
return ""
}

public func deviceCodeUsed(_ deviceCode: OAuthDeviceCode) {}
}
32 changes: 32 additions & 0 deletions Sources/VaporOAuth/Models/OAuthDeviceCode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

public final class OAuthDeviceCode {
public let deviceCodeID: String
public let userCode: String
public let clientID: String
public let userID: String?
public let expiryDate: Date
public let scopes: [String]?

public var extend: [String: Any] = [:]

public init(
deviceCodeID: String,
userCode: String,
clientID: String,
userID: String?,
expiryDate: Date,
scopes: [String]?
) {
self.deviceCodeID = deviceCodeID
self.userCode = userCode
self.clientID = clientID
self.userID = userID
self.expiryDate = expiryDate
self.scopes = scopes
}

public var isExpired: Bool {
return Date() > expiryDate
}
}
3 changes: 3 additions & 0 deletions Sources/VaporOAuth/Protocols/CodeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ public protocol CodeManager {
// This is explicit to ensure that the code is marked as used or deleted (it could be implied that this is done when you call
// `getCode` but it is called explicitly to remind developers to ensure that codes can't be reused)
func codeUsed(_ code: OAuthCode) async throws
func generateDeviceCode(userID: String, clientID: String, scopes: [String]?) async throws -> String
func getDeviceCode(_ deviceCode: String) async throws -> OAuthDeviceCode?
func deviceCodeUsed(_ deviceCode: OAuthDeviceCode) async throws
}
6 changes: 6 additions & 0 deletions Sources/VaporOAuth/RouteHandlers/TokenHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct TokenHandler {
let tokenResponseGenerator: TokenResponseGenerator
let authCodeTokenHandler: AuthCodeTokenHandler
let passwordTokenHandler: PasswordTokenHandler
let deviceCodeTokenHandler: DeviceCodeTokenHandler

init(clientValidator: ClientValidator, tokenManager: TokenManager, scopeValidator: ScopeValidator,
codeManager: CodeManager, userManager: UserManager, logger: Logger) {
Expand All @@ -25,6 +26,9 @@ struct TokenHandler {
passwordTokenHandler = PasswordTokenHandler(clientValidator: clientValidator, scopeValidator: scopeValidator,
userManager: userManager, logger: logger, tokenManager: tokenManager,
tokenResponseGenerator: tokenResponseGenerator)
deviceCodeTokenHandler = DeviceCodeTokenHandler(clientValidator: clientValidator, scopeValidator: scopeValidator, codeManager: codeManager,
tokenManager: tokenManager,
tokenResponseGenerator: tokenResponseGenerator)
}

func handleRequest(request: Request) async throws -> Response {
Expand All @@ -42,6 +46,8 @@ struct TokenHandler {
return try await clientCredentialsTokenHandler.handleClientCredentialsTokenRequest(request)
case OAuthFlowType.refresh.rawValue:
return try await refreshTokenHandler.handleRefreshTokenRequest(request)
case OAuthFlowType.deviceCode.rawValue:
return try await deviceCodeTokenHandler.handleDeviceCodeTokenRequest(request)
default:
return try tokenResponseGenerator.createResponse(error: OAuthResponseParameters.ErrorType.unsupportedGrant,
description: "This server does not support the '\(grantType)' grant type")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Vapor

struct DeviceCodeTokenHandler {

let clientValidator: ClientValidator
let scopeValidator: ScopeValidator
let codeManager: CodeManager
let tokenManager: TokenManager
let tokenResponseGenerator: TokenResponseGenerator

func handleDeviceCodeTokenRequest(_ request: Request) async throws -> Response {
guard let deviceCodeString: String = request.content[OAuthRequestParameters.deviceCode] else {
return try tokenResponseGenerator.createResponse(error: OAuthResponseParameters.ErrorType.invalidRequest,
description: "Request was missing the 'device_code' parameter")
}

guard let clientID: String = request.content[OAuthRequestParameters.clientID] else {
return try tokenResponseGenerator.createResponse(error: OAuthResponseParameters.ErrorType.invalidRequest,
description: "Request was missing the 'client_id' parameter")
}

do {
try await clientValidator.authenticateClient(clientID: clientID, clientSecret: nil,
grantType: .deviceCode)
} catch {
return try tokenResponseGenerator.createResponse(error: OAuthResponseParameters.ErrorType.invalidClient,
description: "Request had invalid client credentials", status: .unauthorized)
}

guard let deviceCode = try await codeManager.getDeviceCode(deviceCodeString) else {
let errorDescription = "The device code provided was invalid or expired"
return try tokenResponseGenerator.createResponse(error: OAuthResponseParameters.ErrorType.invalidGrant,
description: errorDescription)
}

if deviceCode.expiryDate < Date() {
let errorDescription = "The device code provided was invalid or expired"
return try tokenResponseGenerator.createResponse(error: "expired_token",
description: errorDescription)
}

if let scopes = deviceCode.scopes {
do {
try await scopeValidator.validateScope(clientID: clientID, scopes: scopes)
} catch ScopeError.invalid, ScopeError.unknown {
return try tokenResponseGenerator.createResponse(error: OAuthResponseParameters.ErrorType.invalidScope,
description: "Request contained an invalid or unknown scope")
}
}

try await codeManager.deviceCodeUsed(deviceCode)

let expiryTime = 3600

let (access, refresh) = try await tokenManager.generateAccessRefreshTokens(
clientID: clientID, userID: deviceCode.userID,
scopes: deviceCode.scopes,
accessTokenExpiryTime: expiryTime
)

return try tokenResponseGenerator.createResponse(accessToken: access, refreshToken: refresh, expires: Int(expiryTime),
scope: deviceCode.scopes?.joined(separator: " "))
}
}
1 change: 1 addition & 0 deletions Sources/VaporOAuth/Utilities/OAuthFlowType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public enum OAuthFlowType: String {
case clientCredentials = "client_credentials"
case refresh = "refresh_token"
case tokenIntrospection = "token_introspection"
case deviceCode = "urn:ietf:params:oauth:grant-type:device_code"
}
2 changes: 2 additions & 0 deletions Sources/VaporOAuth/Utilities/StringDefines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct OAuthRequestParameters {
static let usernname = "username"
static let csrfToken = "csrfToken"
static let token = "token"
static let deviceCode = "device_code"
}

struct OAuthResponseParameters {
Expand All @@ -39,6 +40,7 @@ struct OAuthResponseParameters {
static let unsupportedGrant = "unsupported_grant_type"
static let invalidGrant = "invalid_grant"
static let missingToken = "missing_token"
static let expiredToken = "expired_token"
}
}

Expand Down
4 changes: 4 additions & 0 deletions Sources/VaporOAuth/Validators/ClientValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ struct ClientValidator {
throw ClientError.notFirstParty
}
}

if grantType == .deviceCode {

}
}

if checkConfidentialClient {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Foundation