Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d50743b
sketch refresh codepath from oauth4web
germ-mark Mar 4, 2026
23a474b
sketch out auth path
germ-mark Mar 4, 2026
a48f1d4
working use of low-level oauth primitives to authenticate
germ-mark Mar 5, 2026
4b72db6
demoing cut over to components mirroring oauth4web
germ-mark Mar 5, 2026
4b35555
shouldn't embed the issuing server in the DPoP proof
germ-mark Mar 6, 2026
a389623
separate out expectSuccess and result decoding
germ-mark Mar 6, 2026
b36ac0b
the jwt only needs request scheme, origin, and path, but explicitly n…
germ-mark Mar 6, 2026
c62db2c
pushedAuthorizationRequest -> .par
germ-mark Mar 6, 2026
7896984
update AuthServerMetadata with full types, courtesy of https://tangle…
germ-mark Mar 7, 2026
fe50622
bsky.social -> social.example in mock server metadata
germ-mark Mar 7, 2026
0a80241
don't need ClientMetadata
germ-mark Mar 7, 2026
4f0baa8
support bearer token, comment out OIDC id_token (still will get picke…
germ-mark Mar 7, 2026
36476d6
Apply suggestion from @ThisIsMissEm
germ-mark Mar 7, 2026
68b29d6
verifier -> pkceVerifier
germ-mark Mar 7, 2026
6a3e2cb
cache the correct response
germ-mark Mar 7, 2026
6c9535d
deprecate AuthServerMetadata.load for authServerDiscovery
germ-mark Mar 7, 2026
5538759
fix typo
germ-mark Mar 7, 2026
219187d
specific error for unsupportedDpopSigningAlgorithm
germ-mark Mar 7, 2026
15192ad
idiomatic resource(for:
germ-mark Mar 7, 2026
82ec500
deprecate Metadata.load, standardize on a URLSession abstraction
germ-mark Mar 7, 2026
44ad392
add client_credentials grant type
germ-mark Mar 8, 2026
6983999
point implement validator to current task for it
germ-mark Mar 8, 2026
3a98d80
document redirect policies
germ-mark Mar 8, 2026
b5f4636
expand and document the OAuthErrorResponse type
germ-mark Mar 8, 2026
6efb454
decode OAuthErrorResponese in processPushedAuthorizationResponse
germ-mark Mar 8, 2026
8b5c355
more deliberately order validateAuthResponse steps
germ-mark Mar 8, 2026
7cd62ca
Merge branch 'main' into mark/oauth4web-shadow
germ-mark Mar 8, 2026
1ae4365
resolve merge conflict with main
germ-mark Mar 8, 2026
d1de495
adjust demo app for merge
germ-mark Mar 8, 2026
34936c8
don't need the resourceFetcher in authorizer
germ-mark Mar 8, 2026
f54887f
route around AuthorizerImpl
germ-mark Mar 9, 2026
f91b9fb
excise AuthRequestable
germ-mark Mar 9, 2026
7999436
consolidate initial parameters for authorization and auth requests (r…
germ-mark Mar 9, 2026
3c0a96e
check sub field in atproto token response
germ-mark Mar 9, 2026
0acde8f
use common authenticated for the try once flow
germ-mark Mar 9, 2026
0bce27a
remove duplicated method
germ-mark Mar 9, 2026
32db775
remove comment
germ-mark Mar 9, 2026
0728178
rename intermediate var
germ-mark Mar 10, 2026
fee9f08
remove unused
germ-mark Mar 10, 2026
d19a6f0
check that received scopes is a subset of requested scopes, falling b…
germ-mark Mar 10, 2026
91c5602
renaming
germ-mark Mar 10, 2026
e6264ed
resolved review comment
germ-mark Mar 10, 2026
1b22cea
pass back additional params to the session state object
germ-mark Mar 11, 2026
e9a4f49
parse error in processGenericAccessToken
germ-mark Mar 11, 2026
a2ba593
for atproto, expect dpop
germ-mark Mar 11, 2026
7467044
error handling in processGenericAccessToken
germ-mark Mar 11, 2026
251124f
fold tokenEndpointRequest into AuthServerRequestOptions
germ-mark Mar 11, 2026
d0fb3be
fix errors
germ-mark Mar 11, 2026
acff9df
add unit test for addProof
germ-mark Mar 11, 2026
07f394a
Merge branch 'main' into mark/oauth4web-shadow
germ-mark Mar 16, 2026
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
22 changes: 10 additions & 12 deletions DemoApp/AtprotoOAuthDemoApp/CachedAuthenticated/LoginVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AtprotoOAuth
import AtprotoTypes
import AuthenticationServices
import Foundation
import GermConvenience
import OAuth
import os

Expand All @@ -27,12 +28,11 @@ import os
callbackURL: URL(string: "com.germnetwork.static:/oauth")!
),
userAuthenticator: ASWebAuthenticationSession.userAuthenticator(),
responseProvider: URLSession.defaultProvider,
resourceFetcher: URLSession.shared,
authFetcher: URLSession.manualRedirect(),
atprotoClient: AtprotoClient(
responseProvider: URLSession.defaultProvider
resourceFetcher: URLSession.shared
),
oauthMetadataFetcher: HTTPOAuthMetadataFetcher(
httpRequester: URLSession.defaultProvider)
)

let handle: String
Expand Down Expand Up @@ -70,12 +70,11 @@ import os
session: sessionArchive,
),
appCredentials: oauthClient.appCredentials,
httpRequester: URLSession.defaultProvider,
resourceFetcher: URLSession.shared,
authFetcher: URLSession.manualRedirect(),
atprotoClient: AtprotoClient(
responseProvider: URLSession.defaultProvider
resourceFetcher: URLSession.shared
),
oauthMetadataFetcher: HTTPOAuthMetadataFetcher(
httpRequester: URLSession.defaultProvider)
)

if !Task.isCancelled {
Expand Down Expand Up @@ -146,12 +145,11 @@ import os
session: archive,
),
appCredentials: oauthClient.appCredentials,
httpRequester: URLSession.defaultProvider,
resourceFetcher: URLSession.shared,
authFetcher: URLSession.manualRedirect(),
atprotoClient: AtprotoClient(
responseProvider: URLSession.defaultProvider
resourceFetcher: URLSession.shared
),
oauthMetadataFetcher: HTTPOAuthMetadataFetcher(
httpRequester: URLSession.defaultProvider)
)
if !Task.isCancelled {
self.session = .init(
Expand Down
18 changes: 8 additions & 10 deletions DemoApp/AtprotoOAuthDemoApp/Login/LoginDemoVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AtprotoOAuth
import AtprotoTypes
import AuthenticationServices
import Foundation
import GermConvenience
import Microcosm
import OAuth
import SwiftUI
Expand All @@ -22,12 +23,11 @@ import SwiftUI
callbackURL: URL(string: "com.germnetwork.static:/oauth")!
),
userAuthenticator: ASWebAuthenticationSession.userAuthenticator(),
responseProvider: URLSession.defaultProvider,
resourceFetcher: URLSession.shared,
authFetcher: URLSession.manualRedirect(),
atprotoClient: AtprotoClient(
responseProvider: URLSession.defaultProvider
resourceFetcher: URLSession.shared
),
oauthMetadataFetcher: HTTPOAuthMetadataFetcher(
httpRequester: URLSession.defaultProvider)
)

enum State {
Expand All @@ -51,7 +51,7 @@ import SwiftUI
logs.append(.init(body: "Resolved DID: \(resolvedDid.fullId)"))

let messageDelegate = try await AtprotoClient(
responseProvider: URLSession.defaultProvider
resourceFetcher: URLSession.shared
)
.getGermMessagingDelegate(did: resolvedDid)

Expand All @@ -73,13 +73,11 @@ import SwiftUI
session: sessionArchive,
),
appCredentials: oauthClient.appCredentials,
httpRequester: URLSession.defaultProvider,
resourceFetcher: URLSession.shared,
authFetcher: URLSession.manualRedirect(),
atprotoClient: AtprotoClient(
responseProvider: URLSession.defaultProvider
resourceFetcher: URLSession.shared
),
oauthMetadataFetcher: HTTPOAuthMetadataFetcher(
httpRequester: URLSession.defaultProvider
)
)
state = .loggedIn(session)

Expand Down
2 changes: 1 addition & 1 deletion DemoApp/AtprotoOAuthDemoApp/UnauthenticatedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct UnauthenticatedView: View {
@State private var processing: Task<Void, Never>? = nil

let client: AtprotoClientInterface = AtprotoClient.init(
responseProvider: URLSession.defaultProvider
resourceFetcher: URLSession.shared
)

var body: some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ public protocol AtprotoSession {
}

public struct AtprotoClient {
let responseProvider: HTTPDataResponse.Requester
let resourceFetcher: HTTPFetcher

public init(responseProvider: @escaping HTTPDataResponse.Requester) {
self.responseProvider = responseProvider
public init(resourceFetcher: HTTPFetcher) {
self.resourceFetcher = resourceFetcher
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ extension AtprotoClient {
let request = URLRequest.createRequest(url: requestURL, httpMethod: .get)

let result = try await session.authResponse(for: request)
.successErrorDecode(
resultType: X.Result.self,
errorType: Lexicon.XRPCError.self
.success(
decodeResult: X.Result.self,
orError: Lexicon.XRPCError.self
)

switch result {
Expand Down Expand Up @@ -51,9 +51,9 @@ extension AtprotoClient {
)

let result = try await session.authResponse(for: request)
.successErrorDecode(
resultType: X.Result.self,
errorType: Lexicon.XRPCError.self
.success(
decodeResult: X.Result.self,
orError: Lexicon.XRPCError.self
)

switch result {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ extension AtprotoClient {
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Accept")

return try await responseProvider(request)
.successDecode()
return try await resourceFetcher.data(for: request)
.expectSuccess()
.decode()
}

private func constructPlcQueryUrl(did: Atproto.DID) throws -> URL {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ extension AtprotoClient {
httpMethod: .get
)

let result = try await responseProvider(request)
.successErrorDecode(
resultType: X.Result.self,
errorType: Lexicon.XRPCError.self
let result = try await resourceFetcher.data(for: request)
.success(
decodeResult: X.Result.self,
orError: Lexicon.XRPCError.self
)

switch result {
Expand Down
1 change: 1 addition & 0 deletions LocalPackages/GermConvenience/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PackageDescription

let package = Package(
name: "GermConvenience",
platforms: [.iOS(.v15), .macOS(.v12)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@

import Foundation

//type the (data, responese) tuple so we can chain handlers
//type the (data, response) tuple so we can chain handlers
//these patterns are available in Vapor
public struct HTTPDataResponse: Sendable {
public let data: Data
public let response: HTTPURLResponse

public typealias Requester = @Sendable (URLRequest) async throws -> HTTPDataResponse

public init(data: Data, response: HTTPURLResponse) {
self.data = data
self.response = response
}

public func successDecode<R: Decodable>() throws -> R {
guard response.statusCode >= 200 && response.statusCode < 300 else {
public func expect(successCode: Int) throws -> Data {
try expectSuccess(range: successCode...successCode)
}

public func expectSuccess(range: RangeExpression<Int> = 200..<300) throws -> Data {
guard range.contains(response.statusCode) else {
if let stringResponse = String(data: data, encoding: .utf8) {
throw
HTTPResponseError
Expand All @@ -30,40 +32,52 @@ public struct HTTPDataResponse: Sendable {
throw HTTPResponseError.unsuccessful(response.statusCode, data)
}
}
return try JSONDecoder().decode(R.self, from: data)
return data
}

public enum ErrorResult<R: Decodable, E: Decodable> {
case result(R)
case error(E, Int)
}

public func successErrorDecode<R: Decodable, E: Decodable>(
resultType: R.Type,
errorType: E.Type
public func success<R: Decodable, E: Decodable>(
code: Int,
decodeResult resultType: R.Type,
orError error: E.Type,
) throws -> ErrorResult<R, E> {
guard response.statusCode >= 200 && response.statusCode < 300 else {
do {
let decoded = try JSONDecoder().decode(E.self, from: data)
return .error(decoded, response.statusCode)
} catch {
if let stringResponse = String(data: data, encoding: .utf8) {
throw
HTTPResponseError
.unsuccessfulString(
response.statusCode, stringResponse)
} else {
throw HTTPResponseError.unsuccessful(
response.statusCode, data)
}
}
try success(
range: code...code,
decodeResult: R.self,
orError: E.self
)
}

public func success<R: Decodable, E: Decodable>(
range: RangeExpression<Int> = 200..<300,
decodeResult resultType: R.Type,
orError error: E.Type,
) throws -> ErrorResult<R, E> {
do {
return .result(
try expectSuccess(range: range)
.decode()
)
} catch {
return .error(try data.decode(), response.statusCode)
}
if resultType == Data?.self || resultType == Data.self,
let rawData = data as? R

}
}

extension Data {
//If the return type is Data we don't try to decode it
public func decode<R: Decodable>() throws -> R {
if R.self == Data?.self || R.self == Data.self,
let rawData = self as? R
{
return .result(rawData)
rawData
} else {
return try .result(JSONDecoder().decode(R.self, from: data))
try JSONDecoder().decode(R.self, from: self)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// HTTPFetcher.swift
// GermConvenience
//
// Created by Mark @ Germ on 3/7/26.
//

import Foundation

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

///We are not genericizing over the network stack as our HTTP request construction relies heavily on
///URLRequest which is tied closely to URLSession.
///This genericization serves to allow mock ingestion for testing
///This is a (sendable) object abstraction, not a closure, so that we can reason about the cache sharing
///which is localiized to an instance of a URLSession
public protocol HTTPFetcher: Sendable {
func data(for: URLRequest) async throws -> HTTPDataResponse
}

///Authorization fetches should not follow redirects. This includes
///the protected resource metadata
///https://www.rfc-editor.org/rfc/rfc9728.html#section-3.2
///and auth server metadata
///https://datatracker.ietf.org/doc/html/rfc8414#section-3.2
extension URLSession {
static public func manualRedirect() -> URLSession {
URLSession(
configuration: .default,
delegate: ManualRedirect(),
delegateQueue: nil
)
}
}

///The default (shared) urlsession does follow redirects, which is permitted for resource requests
extension URLSession: HTTPFetcher {
public func data(for request: URLRequest) async throws -> HTTPDataResponse {
let (data, urlResponse) = try await self.data(for: request)
if let httpResponse = urlResponse as? HTTPURLResponse {
return .init(data: data, response: httpResponse)
} else {
throw URLSessionError.nonHttpResponse
}
}
}

enum URLSessionError: Error {
case nonHttpResponse
}

final class ManualRedirect: NSObject, URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest
) async -> URLRequest? {
nil
}
}
2 changes: 1 addition & 1 deletion LocalPackages/oauth4swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription

let package = Package(
name: "OAuth",
platforms: [.iOS(.v15), .macOS(.v12)],
platforms: [.iOS(.v16), .macOS(.v13)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
Expand Down
Loading
Loading