Skip to content
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
1 change: 0 additions & 1 deletion Sources/OAuthKit/Extensions/URLRequest+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public extension URLRequest {

/// Attempts to set the authorization header using the auth token.
/// - Parameter auth: the oauth authorization
@MainActor
mutating func addAuthorization(auth: OAuth.Authorization) {
addValue("\(auth.token.type) \(auth.token.accessToken)", forHTTPHeaderField: authHeader)
}
Expand Down
10 changes: 9 additions & 1 deletion Sources/OAuthKit/OAuth+Provider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ extension OAuth {
public var customUserAgent: String?
/// Enables provider debugging. Off by default.
public var debug: Bool
/// An optional regex that can be used in conjuction with the ``OAuth/URLProtocol`` class to intercept outbound requests
/// and inject an `Authorization: Bearer <<token>>` header into a request.
public var authorizationPattern: String?

/// The coding keys.
enum CodingKeys: String, CodingKey {
Expand All @@ -55,6 +58,7 @@ extension OAuth {
case encodeHttpBody
case customUserAgent
case debug
case authorizationPattern
}

/// Public initializer.
Expand All @@ -71,6 +75,7 @@ extension OAuth {
/// - encodeHttpBody: If the provider should encode the access token parameters into the http body (true by default)
/// - customUserAgent: The custom user agent to send with browser requests.
/// - debug: Boolean to pass debugging into to the standard output (false by default)
/// - authorizationPattern: a regex pattern used to automatically match outbound url requests that should have authorization headers added
public init(id: String,
icon: URL? = nil,
authorizationURL: URL,
Expand All @@ -82,7 +87,8 @@ extension OAuth {
scope: [String]? = nil,
encodeHttpBody: Bool = true,
customUserAgent: String? = nil,
debug: Bool = false) {
debug: Bool = false,
authorizationPattern: String? = nil) {
self.id = id
self.icon = icon
self.authorizationURL = authorizationURL
Expand All @@ -95,6 +101,7 @@ extension OAuth {
self.encodeHttpBody = encodeHttpBody
self.customUserAgent = customUserAgent
self.debug = debug
self.authorizationPattern = authorizationPattern
}

/// Custom decoder initializer.
Expand All @@ -114,6 +121,7 @@ extension OAuth {
encodeHttpBody = try container.decodeIfPresent(Bool.self, forKey: .encodeHttpBody) ?? true
customUserAgent = try container.decodeIfPresent(String.self, forKey: .customUserAgent)
debug = try container.decodeIfPresent(Bool.self, forKey: .debug) ?? false
authorizationPattern = try container.decodeIfPresent(String.self, forKey: .authorizationPattern)
}
}
}
86 changes: 86 additions & 0 deletions Sources/OAuthKit/OAuth+URLProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// OAuth+URLProtocol.swift
// OAuthKit
//
// Created by Kevin McKee
//

import Foundation

extension OAuth {

/// A custom `URLProtocol` that can be registered with any `URLSessionConfiguration` that will automatically inject
/// `Authorization: Bearer <<token>>` headers into outbound HTTP URLRequests based on ``Provider/authorizationPattern``.
public class URLProtocol: Foundation.URLProtocol {

/// The lock that provides manual synchronization around access to authorization tokens.
private static let lock: NSLock = .init()
nonisolated(unsafe) private static var _authorizations: [OAuth.Provider: OAuth.Authorization] = .init()

static var authorizations: [OAuth.Provider: OAuth.Authorization] {
get { lock.withLock { _authorizations } }
set { lock.withLock { _authorizations = newValue } }
}

/// Adds an authorization for the given provider that can be used inject `Authorization: Bearer <<token>>` headers into a request.
/// - Parameters:
/// - authorization: the authorization issued by the provider
/// - provider: the provider
@MainActor
public class func addAuthorization(_ authorization: OAuth.Authorization, for provider: OAuth.Provider) {
guard let _ = provider.authorizationPattern else { return }
authorizations[provider] = authorization
}

/// Removes authorizations for the specified provider.
/// - Parameter provider: the provider to remove authorization for
@MainActor
public class func removeAuthorization(for provider: OAuth.Provider) {
authorizations.removeValue(forKey: provider)
}

/// Clears all authorizations out of the protocol.
@MainActor
public class func clear() {
authorizations.removeAll()
}

/// Determines whether this protocol can handle the given request.
/// - Parameter request: the request to handle
/// - Returns: always true
override public class func canInit(with request: URLRequest) -> Bool {
// Remove any expired authorizations
let expiredEntries = authorizations.filter{ $0.value.isExpired }
for expired in expiredEntries {
authorizations.removeValue(forKey: expired.key)
}
guard let url = request.url, authorizations.isNotEmpty else { return false }
for (provider, _) in authorizations {
guard let pattern = provider.authorizationPattern else { continue }
if url.absoluteString.range(of: pattern, options: .regularExpression) != nil {
return true
}
}
return false
}

/// If an authorized provider matches the given request, then this method returns a canonical version
/// of the given request with an additional `Authorization: Bearer <<token>>` header field.
/// - Parameter request: the request
/// - Returns: the canonical version of the given request.
override public class func canonicalRequest(for request: URLRequest) -> URLRequest {
guard let url = request.url, authorizations.isNotEmpty else { return request }
for (provider, auth) in authorizations {
guard let pattern = provider.authorizationPattern else { continue }
if url.absoluteString.range(of: pattern, options: .regularExpression) != nil {
var canonicalRequest = request
canonicalRequest.addAuthorization(auth: auth)
return canonicalRequest
}
}
return request
}
}
}


10 changes: 10 additions & 0 deletions Sources/OAuthKit/OAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public extension OAuth {
func clear() {
debugPrint("⚠️ [Clearing oauth state]")
keychain.clear()
URLProtocol.clear()
state = .empty
}
}
Expand Down Expand Up @@ -277,6 +278,7 @@ private extension OAuth {
func publish(state: State) {
switch state {
case .authorized(let provider, let auth):
addAuthorization(provider: provider, authorization: auth)
schedule(provider: provider, auth: auth)
case .receivedDeviceCode(let provider, let deviceCode):
schedule(provider: provider, deviceCode: deviceCode)
Expand All @@ -286,6 +288,14 @@ private extension OAuth {
self.state = state
}

/// Adds a received authorization to the ``OAuth/URLProtocol`` class.
/// - Parameters:
/// - provider: the provider
/// - authorization: the authorization issued by the provider
func addAuthorization(provider: Provider, authorization: Authorization) {
URLProtocol.addAuthorization(authorization, for: provider)
}

/// Schedules the provider to be polled for authorization with the specified device token.
/// - Parameters:
/// - provider: the oauth provider
Expand Down
2 changes: 1 addition & 1 deletion Sources/OAuthKit/OAuthKit.docc/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Explore advanced OAuth configuration options such as Keychain protection and pri
purpose: card,
source: "config-card",
alt: "OAuthKit Framework Configuration")
@PageColor(red)
@PageColor(purple)
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
@Available(tvOS, introduced: "18.0")
Expand Down
1 change: 0 additions & 1 deletion Sources/OAuthKit/OAuthKit.docc/Extensions/Authorization.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# ``OAuthKit/OAuth/Authorization``

@Metadata {
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
Expand Down
1 change: 0 additions & 1 deletion Sources/OAuthKit/OAuthKit.docc/Extensions/GrantType.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# ``OAuthKit/OAuth/GrantType``

@Metadata {
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
Expand Down
9 changes: 9 additions & 0 deletions Sources/OAuthKit/OAuthKit.docc/Extensions/NetworkMonitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# ``OAuthKit/NetworkMonitor``
@Metadata {
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
@Available(tvOS, introduced: "18.0")
@Available(visionOS, introduced: "2.0")
@Available(watchOS, introduced: "11.0")
}

3 changes: 3 additions & 0 deletions Sources/OAuthKit/OAuthKit.docc/Extensions/OAuth.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ let oauth: OAuth = .init(.module, options: options)

- ``state``
- ``providers``

### OAuth Authorization Headers
- ``OAuthKit/OAuth/URLProtocol``
1 change: 0 additions & 1 deletion Sources/OAuthKit/OAuthKit.docc/Extensions/Provider.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# ``OAuthKit/OAuth/Provider``

@Metadata {
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
Expand Down
1 change: 0 additions & 1 deletion Sources/OAuthKit/OAuthKit.docc/Extensions/State.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# ``OAuthKit/OAuth/State``

@Metadata {
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
Expand Down
1 change: 0 additions & 1 deletion Sources/OAuthKit/OAuthKit.docc/Extensions/Token.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# ``OAuthKit/OAuth/Token``

@Metadata {
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
Expand Down
20 changes: 20 additions & 0 deletions Sources/OAuthKit/OAuthKit.docc/Extensions/URLProtocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ``OAuthKit/OAuth/URLProtocol``
@Metadata {
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
@Available(tvOS, introduced: "18.0")
@Available(visionOS, introduced: "2.0")
@Available(watchOS, introduced: "11.0")
}

## Overview
The URLProtocol can be registered with any `URLSessionConfiguration` and will automatically inject `Authorization` headers into outbound HTTP URLRequests if patterns are matched.

### Tutorials

@Links(visualStyle: list) {
- <doc:URLProtocol>
}

- SeeAlso:
[Making Authenticated Requests](https://www.oauth.com/oauth2-servers/making-authenticated-requests/)
14 changes: 9 additions & 5 deletions Sources/OAuthKit/OAuthKit.docc/GettingStarted.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# Getting Started with OAuthKit

Learn how to create an observable OAuth instance and start an authorization flow.

@Metadata {
@PageKind(article)
@PageImage(
purpose: card,
source: "gettingStarted-card",
alt: "Getting Started with OAuthKit")
@PageColor(yellow)
@PageColor(purple)
@Available(iOS, introduced: "18.0")
@Available(macOS, introduced: "15.0")
@Available(tvOS, introduced: "18.0")
@Available(visionOS, introduced: "2.0")
@Available(watchOS, introduced: "11.0")
}

Learn how to create an observable OAuth instance and start an authorization flow.


## Overview

OAuth 2.0 authorization flows are started by calling ``OAuth/authorize(provider:grantType:)`` with an ``OAuth/Provider`` and ``OAuth/GrantType``.
Expand Down Expand Up @@ -151,7 +151,7 @@ func handle(state: OAuth.State) {
```
Once the user has successfully authorized with the ``OAuth/Provider``, the ``OAuth/state`` will move to an ``OAuth/State/authorized(_:_:)`` state. You can then automaticaly close the ``OAWebView`` window in your SwiftUI application.

> Tip: Once authorized, a ``OAuth/Authorization/token`` will be inserted into the user's Keychain that can be used in subsequent API requests by inserting the `Authorization` header into an ``Foundation/URLRequest`` via ``OAuthKit/Foundation/URLRequest/addAuthorization(auth:)`` .
> Tip: Once authorized, a ``OAuth/Authorization/token`` will be inserted into the user's Keychain that can be used in subsequent API requests by inserting the `Authorization` header into an `URLRequest` via ``OAuthKit/Foundation/URLRequest/addAuthorization(auth:)`` .

## Presenting Device Codes (tvOS and watchOS)
OAuthKit also supports the [OAuth 2.0 Device Code Flow Grant](https://alexbilbie.github.io/2016/04/oauth-2-device-flow-grant/), which is used by apps that don't have access to a web browser (like tvOS or watchOS). To leverage OAuthKit in tvOS or watchOS apps, simply add the `deviceCodeURL` to your ``OAuth/Provider`` and start an ``OAuth/authorize(provider:grantType:)`` flow with the ``OAuth/GrantType/deviceCode`` grantType.
Expand Down Expand Up @@ -188,4 +188,8 @@ The observable ``OAuth`` instance will proceed to poll the ``OAuth/Provider`` ac

> Tip: Click [here](https://oauth.net/2/grant-types/device-code/) to see details of how the OAuth 2.0 Device Code Grant Type works.

### Related Tutorials

@Links(visualStyle: list) {
- <doc:FlowsStates>
}
11 changes: 8 additions & 3 deletions Sources/OAuthKit/OAuthKit.docc/OAuthKit.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# ``OAuthKit``

A modern and observable framework for implementing OAuth 2.0 authorization flows.

@Metadata {
@PageColor(purple)
@Available(iOS, introduced: "18.0")
Expand All @@ -11,6 +8,8 @@ A modern and observable framework for implementing OAuth 2.0 authorization flows
@Available(watchOS, introduced: "11.0")
}

A modern and observable framework for implementing OAuth 2.0 authorization flows.

## Overview

OAuthKit offers a robust, type-safe, and performant OAuth 2.0 implementation using the observer design pattern. This pattern allows applications to observe an ``OAuth`` object and be notified of ``OAuth/State`` changes.
Expand All @@ -30,6 +29,12 @@ This has the advantage of avoiding direct coupling with an authorization flow, e
- <doc:SampleCode>
}

### Tutorials

@Links(visualStyle: list) {
- <doc:Contents>
}

## Topics

### Essentials
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import OAuthKit
import SwiftUI

@main
struct OAuthApp: App {

@Environment(\.oauth)
var oauth: OAuth

/// Build the scene body
var body: some Scene {

// The main window
WindowGroup {
ContentView()
}

// The authorization window
WindowGroup(id: "oauth") {
OAWebView(oauth: oauth)
}
}
}
Loading