-
Notifications
You must be signed in to change notification settings - Fork 2
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
Add authentication before checking for updates #6
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
fe2b7b0
Add login
noahsmartin 32dfb32
Merge branch 'main' of github.com:EmergeTools/ETDistribution into fea…
Itaybre a2f7e8e
Save and refresh access tokens
Itaybre 2ddbd4d
Add Login Examples
Itaybre 30d115d
Open url in main thread
Itaybre 95f4e74
Flip boolean
Itaybre 3a9f03a
Remove completion and use audience
Itaybre f7b126b
Set access token
Itaybre 9dd6e37
Merge branch 'main' of github.com:EmergeTools/ETDistribution into fea…
Itaybre File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
// | ||
// Auth.swift | ||
// ETDistribution | ||
// | ||
// Created by Noah Martin on 9/23/24. | ||
// | ||
|
||
import AuthenticationServices | ||
import CommonCrypto | ||
|
||
enum LoginError: Error { | ||
case noUrl | ||
case noCode | ||
case invalidData | ||
} | ||
|
||
enum Auth { | ||
private enum Constants { | ||
static let url = URL(string: "https://auth.emergetools.com")! | ||
static let clientId = "XiFbzCzBHV5euyxbcxNHbqOHlKcTwzBX" | ||
static let redirectUri = URL(string: "app.install.callback://callback")! | ||
static let accessTokenKey = "accessToken" | ||
static let refreshTokenKey = "refreshToken" | ||
} | ||
|
||
static func getAccessToken(settings: CheckForUpdateParams.LoginSetting, completion: @escaping (Result<String, Error>) -> Void) { | ||
if let token = KeychainHelper.getToken(key: Constants.accessTokenKey), | ||
JWTHelper.isValid(token: token) { | ||
completion(.success(token)) | ||
} else if let refreshToken = KeychainHelper.getToken(key: Constants.refreshTokenKey) { | ||
refreshAccessToken(refreshToken) { result in | ||
switch result { | ||
case .success(let accessToken): | ||
completion(.success(accessToken)) | ||
case .failure(let error): | ||
requestLogin(settings, completion) | ||
} | ||
} | ||
} else { | ||
requestLogin(settings, completion) | ||
} | ||
} | ||
|
||
private static func requestLogin(_ settings: CheckForUpdateParams.LoginSetting, _ completion: @escaping (Result<String, Error>) -> Void) { | ||
login(settings: settings) { result in | ||
switch result { | ||
case .success(let response): | ||
do { | ||
try KeychainHelper.setToken(response.accessToken, key: Constants.accessTokenKey) | ||
try KeychainHelper.setToken(response.refreshToken, key: Constants.refreshTokenKey) | ||
completion(.success(response.accessToken)) | ||
} catch { | ||
completion(.failure(error)) | ||
} | ||
case .failure(let error): | ||
completion(.failure(error)) | ||
} | ||
} | ||
} | ||
|
||
private static func refreshAccessToken(_ refreshToken: String, completion: @escaping (Result<String, Error>) -> Void) { | ||
let url = URL(string: "oauth/token", relativeTo: Constants.url)! | ||
|
||
let parameters = [ | ||
"grant_type": "refresh_token", | ||
"client_id": Constants.clientId, | ||
"refresh_token": refreshToken, | ||
] | ||
|
||
var request = URLRequest(url: url) | ||
request.httpMethod = "POST" | ||
request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
request.httpBody = try! JSONSerialization.data(withJSONObject: parameters, options: []) | ||
|
||
URLSession(configuration: URLSessionConfiguration.ephemeral).refreshAccessToken(request) { result in | ||
switch result { | ||
case .success(let response): | ||
do { | ||
try KeychainHelper.setToken(response.accessToken, key: Constants.accessTokenKey) | ||
completion(.success(response.accessToken)) | ||
} catch { | ||
completion(.failure(error)) | ||
} | ||
case .failure(let error): | ||
completion(.failure(error)) | ||
} | ||
} | ||
} | ||
|
||
private static func login( | ||
settings: CheckForUpdateParams.LoginSetting, | ||
completion: @escaping (Result<AuthCodeResponse, Error>) -> Void) | ||
{ | ||
let verifier = getVerifier()! | ||
let challenge = getChallenge(for: verifier)! | ||
|
||
let authorize = URL(string: "authorize", relativeTo: Constants.url)! | ||
var components = URLComponents(url: authorize, resolvingAgainstBaseURL: true)! | ||
var items: [URLQueryItem] = [] | ||
var entries: [String: String] = [:] | ||
|
||
entries["scope"] = "openid profile email offline_access" | ||
entries["client_id"] = Constants.clientId | ||
entries["response_type"] = "code" | ||
if case .connection(let string) = settings { | ||
entries["connection"] = string | ||
} | ||
entries["redirect_uri"] = Constants.redirectUri.absoluteString | ||
entries["state"] = generateDefaultState() | ||
entries["audience"] = "https://auth0-jwt-authorizer" | ||
entries.forEach { items.append(URLQueryItem(name: $0, value: $1)) } | ||
components.queryItems = items | ||
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B") | ||
|
||
let url = components.url! | ||
let session = ASWebAuthenticationSession( | ||
url: url, | ||
callbackURLScheme: Constants.redirectUri.scheme!) { url, error in | ||
if let error { | ||
completion(.failure(error)) | ||
return | ||
} | ||
|
||
if let url { | ||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) | ||
let code = components!.queryItems!.first(where: { $0.name == "code"}) | ||
if let code { | ||
self.exchangeAuthorizationCodeForTokens(authorizationCode: code.value!, verifier: verifier) { result in | ||
completion(result) | ||
} | ||
} else { | ||
completion(.failure(LoginError.noCode)) | ||
} | ||
} else { | ||
completion(.failure(LoginError.noUrl)) | ||
} | ||
} | ||
session.presentationContextProvider = PresentationContextProvider.shared | ||
session.start() | ||
} | ||
|
||
private static func exchangeAuthorizationCodeForTokens( | ||
authorizationCode: String, | ||
verifier: String, | ||
completion: @escaping (Result<AuthCodeResponse, Error>) -> Void) | ||
{ | ||
let url = URL(string: "oauth/token", relativeTo: Constants.url)! | ||
|
||
let parameters = [ | ||
"grant_type": "authorization_code", | ||
"code_verifier": verifier, | ||
"client_id": Constants.clientId, | ||
"code": authorizationCode, | ||
"redirect_uri": Constants.redirectUri.absoluteString, | ||
] | ||
|
||
var request = URLRequest(url: url) | ||
request.httpMethod = "POST" | ||
request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
request.httpBody = try! JSONSerialization.data(withJSONObject: parameters, options: []) | ||
|
||
URLSession(configuration: URLSessionConfiguration.ephemeral).getAuthDataWith(request, completion: completion) | ||
} | ||
|
||
private static func getVerifier() -> String? { | ||
let data = Data(count: 32) | ||
var tempData = data | ||
_ = tempData.withUnsafeMutableBytes { | ||
SecRandomCopyBytes(kSecRandomDefault, data.count, $0.baseAddress!) | ||
} | ||
return tempData.a0_encodeBase64URLSafe() | ||
} | ||
|
||
private static func getChallenge(for verifier: String) -> String? { | ||
guard let data = verifier.data(using: .utf8) else { return nil } | ||
|
||
var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) | ||
_ = data.withUnsafeBytes { | ||
CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer) | ||
} | ||
return Data(buffer).a0_encodeBase64URLSafe() | ||
} | ||
|
||
private static func generateDefaultState() -> String { | ||
let data = Data(count: 32) | ||
var tempData = data | ||
|
||
let result = tempData.withUnsafeMutableBytes { | ||
SecRandomCopyBytes(kSecRandomDefault, data.count, $0.baseAddress!) | ||
} | ||
|
||
guard result == 0, let state = tempData.a0_encodeBase64URLSafe() | ||
else { return UUID().uuidString.replacingOccurrences(of: "-", with: "") } | ||
|
||
return state | ||
} | ||
} | ||
|
||
private class PresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding { | ||
fileprivate static let shared = PresentationContextProvider() | ||
|
||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { | ||
if | ||
let windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene, | ||
let window = windowScene.windows.first(where: \.isKeyWindow) { | ||
return window | ||
} | ||
return ASPresentationAnchor() | ||
} | ||
} | ||
|
||
extension Data { | ||
fileprivate func a0_encodeBase64URLSafe() -> String? { | ||
return self | ||
.base64EncodedString(options: []) | ||
.replacingOccurrences(of: "+", with: "-") | ||
.replacingOccurrences(of: "/", with: "_") | ||
.replacingOccurrences(of: "=", with: "") | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to make this configurable so users can pass in their own
ASWebAuthenticationPresentationContextProviding