Skip to content

Commit

Permalink
Add authentication before checking for updates (#6)
Browse files Browse the repository at this point in the history
* Add login

* Save and refresh access tokens

* Add Login Examples

* Open url in main thread

* Flip boolean

* Remove completion and use audience

* Set access token

---------

Co-authored-by: Noah Martin <[email protected]>
  • Loading branch information
Itaybre and noahsmartin authored Nov 13, 2024
1 parent dea1291 commit b2d32c3
Show file tree
Hide file tree
Showing 11 changed files with 529 additions and 49 deletions.
56 changes: 36 additions & 20 deletions Example/DemoApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,44 @@
import SwiftUI

struct ContentView: View {
var body: some View {
VStack(spacing: 20.0) {
Button("Check For Update Swift") {
UpdateUtil.checkForUpdates()
}
.padding()
.background(.blue)
.foregroundColor(.white)
.cornerRadius(10)

Button("Check For Update ObjC") {
UpdateUtilObjc().checkForUpdates()
}
.padding()
.background(.gray)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding()
var body: some View {
VStack(spacing: 20.0) {
Button("Check For Update Swift") {
UpdateUtil.checkForUpdates()
}
.padding()
.background(.blue)
.foregroundColor(.white)
.cornerRadius(10)

Button("Check For Update With Login Swift") {
UpdateUtil.checkForUpdatesWithLogin()
}
.padding()
.background(.orange)
.foregroundColor(.white)
.cornerRadius(10)

Button("Check For Update ObjC") {
UpdateUtilObjc().checkForUpdates()
}
.padding()
.background(.gray)
.foregroundColor(.white)
.cornerRadius(10)

Button("Check For Update With Login ObjC") {
UpdateUtilObjc().checkForUpdatesWithLogin()
}
.padding()
.background(.yellow)
.foregroundColor(.black)
.cornerRadius(10)
}
.padding()
}
}

#Preview {
ContentView()
ContentView()
}
54 changes: 40 additions & 14 deletions Example/DemoApp/UpdateUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,48 @@ import ETDistribution
struct UpdateUtil {
static func checkForUpdates() {
ETDistribution.shared.checkForUpdate(params: CheckForUpdateParams(apiKey: Constants.apiKey)) { result in
switch result {
case .success(let releaseInfo):
if let releaseInfo {
print("Update found: \(releaseInfo)")
guard let url = ETDistribution.shared.buildUrlForInstall(releaseInfo.downloadUrl) else {
return
}
UIApplication.shared.open(url) { _ in
exit(0)
}
} else {
print("Already up to date")
switch result {
case .success(let releaseInfo):
if let releaseInfo {
print("Update found: \(releaseInfo)")
guard let url = ETDistribution.shared.buildUrlForInstall(releaseInfo.downloadUrl) else {
return
}
DispatchQueue.main.async {
UIApplication.shared.open(url) { _ in
exit(0)
}
case .failure(let error):
print("Error checking for update: \(error)")
}
} else {
print("Already up to date")
}
case .failure(let error):
print("Error checking for update: \(error)")
}
}
}

static func checkForUpdatesWithLogin() {
let params = CheckForUpdateParams(apiKey: Constants.apiKey, requiresLogin: true)
ETDistribution.shared.checkForUpdate(params: params) { result in
switch result {
case .success(let releaseInfo):
if let releaseInfo {
print("Update found: \(releaseInfo)")
guard let url = ETDistribution.shared.buildUrlForInstall(releaseInfo.downloadUrl) else {
return
}
DispatchQueue.main.async {
UIApplication.shared.open(url) { _ in
exit(0)
}
}
} else {
print("Already up to date")
}
case .failure(let error):
print("Error checking for update: \(error)")
}
}
}
}
3 changes: 2 additions & 1 deletion Example/DemoApp/UpdateUtilObjc.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN

@interface UpdateUtilObjc : NSObject
- (void) checkForUpdates;
- (void) checkForUpdatesWithLogin;
@end

NS_ASSUME_NONNULL_END
NS_ASSUME_NONNULL_END
11 changes: 11 additions & 0 deletions Example/DemoApp/UpdateUtilObjc.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,15 @@ - (void) checkForUpdates {
NSLog(@"Error checking for update: %@", error);
}];
}

- (void) checkForUpdatesWithLogin {
CheckForUpdateParams *params = [[CheckForUpdateParams alloc] initWithApiKey:[Constants apiKey] tagName:[Constants tagName] requiresLogin:YES];
[[ETDistribution sharedInstance] checkForUpdateWithParams:params
onReleaseAvailable:^(DistributionReleaseInfo *releaseInfo) {
NSLog(@"Release info: %@", releaseInfo);
}
onError:^(NSError *error) {
NSLog(@"Error checking for update: %@", error);
}];
}
@end
220 changes: 220 additions & 0 deletions Sources/Auth.swift
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: "")
}
}
Loading

0 comments on commit b2d32c3

Please sign in to comment.