Skip to content

Commit

Permalink
feat: Add async OIDCAuthProvider protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
royjit committed Apr 14, 2023
1 parent fa14604 commit 1c1218e
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,41 @@ extension RealtimeConnectionProvider: ConnectionInterceptable {

public func interceptConnection(
_ request: AppSyncConnectionRequest,
for endpoint: URL
) -> AppSyncConnectionRequest {
let finalRequest = connectionInterceptors.reduce(request) { $1.interceptConnection($0, for: endpoint) }
return finalRequest
for endpoint: URL,
completion: (AppSyncConnectionRequest) -> Void) {

chainInterceptors(
index: 0,
request: request,
endpoint: endpoint,
completion: completion)
}

private func chainInterceptors(
index: Int,
request: AppSyncConnectionRequest,
endpoint: URL,
completion: (AppSyncConnectionRequest) -> Void) {

guard index < connectionInterceptors.count else {
completion(request)
return
}
let interceptor = connectionInterceptors[index]
interceptor.interceptConnection(request, for: endpoint) { interceptedRequest in
chainInterceptors(
index: index + 1,
request: interceptedRequest,
endpoint: endpoint,
completion: completion)
}
}

// MARK: Deprecated method

public func interceptConnection(
_ request: AppSyncConnectionRequest,
for endpoint: URL) -> AppSyncConnectionRequest {
fatalError("Should not be invoked, use the callback based api")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,41 @@ extension RealtimeConnectionProvider: MessageInterceptable {
messageInterceptors.append(interceptor)
}

public func interceptMessage(
_ message: AppSyncMessage,
for endpoint: URL,
completion: (AppSyncMessage) -> Void) {

chainInterceptors(
index: 0,
message: message,
endpoint: endpoint,
completion: completion)
}

private func chainInterceptors(
index: Int,
message: AppSyncMessage,
endpoint: URL,
completion: (AppSyncMessage) -> Void) {

guard index < messageInterceptors.count else {
completion(message)
return
}
let interceptor = messageInterceptors[index]
interceptor.interceptMessage(message, for: endpoint) { interceptedMessage in
chainInterceptors(
index: index + 1,
message: interceptedMessage,
endpoint: endpoint,
completion: completion)
}
}

// MARK: Deprecated method

public func interceptMessage(_ message: AppSyncMessage, for endpoint: URL) -> AppSyncMessage {
let finalMessage = messageInterceptors.reduce(message) { $1.interceptMessage($0, for: endpoint) }
return finalMessage
fatalError("Should not be invoked, use the callback based api")
}
}
142 changes: 87 additions & 55 deletions AppSyncRealTimeClient/Interceptor/OIDCAuthInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,70 +15,102 @@ public class OIDCAuthInterceptor: AuthInterceptor {
self.authProvider = authProvider
}

public func interceptMessage(_ message: AppSyncMessage, for endpoint: URL) -> AppSyncMessage {
let host = endpoint.host!
let jwtToken: String
switch authProvider.getLatestAuthToken() {
case .success(let token):
jwtToken = token
case .failure:
return message
}
switch message.messageType {
case .subscribe:
let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host)
var payload = message.payload ?? AppSyncMessage.Payload()
payload.authHeader = authHeader

let signedMessage = AppSyncMessage(
id: message.id,
payload: payload,
type: message.messageType
)
return signedMessage
default:
break
public func interceptMessage(
_ message: AppSyncMessage,
for endpoint: URL,
completion: (AppSyncMessage) -> Void) {
guard let host = endpoint.host else {
completion(message)
return
}

authProvider.getLatestAuthToken { result in
let jwtToken: String
switch result {
case .success(let token):
jwtToken = token
case .failure:
completion(message)
return
}
guard case .subscribe = message.messageType else {
completion(message)
return
}
let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host)
var payload = message.payload ?? AppSyncMessage.Payload()
payload.authHeader = authHeader

let signedMessage = AppSyncMessage(
id: message.id,
payload: payload,
type: message.messageType
)
completion(signedMessage)
return
}
}
return message
}

public func interceptConnection(
_ request: AppSyncConnectionRequest,
for endpoint: URL
) -> AppSyncConnectionRequest {
let host = endpoint.host!
let jwtToken: String
switch authProvider.getLatestAuthToken() {
case .success(let token):
jwtToken = token
case .failure:
// A user that is not signed in should receive an unauthorized error from the connection attempt. This code
// achieves this by always creating a valid request to AppSync even when the token cannot be retrieved. The
// request sent to AppSync will receive a response indicating the request is unauthorized. If we do not use
// empty token string and perform the remaining logic of the request construction then it will fail request
// validation at AppSync before the authorization check, which ends up being propagated back to the caller
// as a "bad request". Example of bad requests are when the header and payload query strings are missing
// or when the data is not base64 encoded.
jwtToken = ""
}
for endpoint: URL,
completion: (AppSyncConnectionRequest) -> Void) {

guard let host = endpoint.host else {
completion(request)
return
}

let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host)
let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader)
authProvider.getLatestAuthToken { result in
let jwtToken: String
switch result {
case .success(let token):
jwtToken = token
case .failure:
// A user that is not signed in should receive an unauthorized error from the connection attempt. This code
// achieves this by always creating a valid request to AppSync even when the token cannot be retrieved. The
// request sent to AppSync will receive a response indicating the request is unauthorized. If we do not use
// empty token string and perform the remaining logic of the request construction then it will fail request
// validation at AppSync before the authorization check, which ends up being propagated back to the caller
// as a "bad request". Example of bad requests are when the header and payload query strings are missing
// or when the data is not base64 encoded.
jwtToken = ""
}

let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8)
let payloadBase64 = payloadData?.base64EncodedString()
let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host)
let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader)

guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else {
return request
let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8)
let payloadBase64 = payloadData?.base64EncodedString()

guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else {
completion(request)
return
}
let headerQuery = URLQueryItem(name: RealtimeProviderConstants.header, value: base64Auth)
let payloadQuery = URLQueryItem(name: RealtimeProviderConstants.payload, value: payloadBase64)
urlComponents.queryItems = [headerQuery, payloadQuery]
guard let url = urlComponents.url else {
completion(request)
return
}
let signedRequest = AppSyncConnectionRequest(url: url)
completion(signedRequest)
return
}
}
let headerQuery = URLQueryItem(name: RealtimeProviderConstants.header, value: base64Auth)
let payloadQuery = URLQueryItem(name: RealtimeProviderConstants.payload, value: payloadBase64)
urlComponents.queryItems = [headerQuery, payloadQuery]
guard let url = urlComponents.url else {
return request

public func interceptMessage(
_ message: AppSyncMessage,
for endpoint: URL) -> AppSyncMessage {
fatalError("")
}
let signedRequest = AppSyncConnectionRequest(url: url)
return signedRequest

public func interceptConnection(
_ request: AppSyncConnectionRequest,
for endpoint: URL
) -> AppSyncConnectionRequest {
fatalError("")
}
}

Expand Down
10 changes: 10 additions & 0 deletions AppSyncRealTimeClient/Support/OIDCAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,14 @@

public protocol OIDCAuthProvider {
func getLatestAuthToken() -> Result<String, Error>

func getLatestAuthToken(completion: (Result<String, Error>) -> Void )
}

public extension OIDCAuthProvider {

func getLatestAuthToken(completion: (Result<String, Error>) -> Void) {
let result = getLatestAuthToken()
completion(result)
}
}

0 comments on commit 1c1218e

Please sign in to comment.