From 5a1bef7412af8f7f4ae96cada7cca6f580524481 Mon Sep 17 00:00:00 2001 From: Newman Chow Date: Fri, 23 Feb 2024 22:33:41 +0800 Subject: [PATCH] Use x_settings_action for change password #161 --- Sources/APIClient.swift | 12 +++ Sources/Authgear.swift | 183 ++++++++++++++++++++++++++++++---- example/ios_example/App.swift | 16 ++- 3 files changed, 188 insertions(+), 23 deletions(-) diff --git a/Sources/APIClient.swift b/Sources/APIClient.swift index 167e8e4a..e8309975 100644 --- a/Sources/APIClient.swift +++ b/Sources/APIClient.swift @@ -7,6 +7,13 @@ enum GrantType: String { case biometric = "urn:authgear:params:oauth:grant-type:biometric-request" case idToken = "urn:authgear:params:oauth:grant-type:id-token" case app2app = "urn:authgear:params:oauth:grant-type:app2app-request" + case settingsAction = "urn:authgear:params:oauth:grant-type:settings-action" +} + +enum ResponseType: String { + case code + case settingsAction = "urn:authgear:params:oauth:response-type:settings-action" + case none } struct APIResponse: Decodable { @@ -36,6 +43,7 @@ struct OIDCAuthenticationRequest { let maxAge: Int? let wechatRedirectURI: String? let page: AuthenticationPage? + let settingsAction: SettingsAction? func toQueryItems(clientID: String, verifier: CodeVerifier?) -> [URLQueryItem] { var queryItems = [ @@ -108,6 +116,10 @@ struct OIDCAuthenticationRequest { queryItems.append(URLQueryItem(name: "x_page", value: page.rawValue)) } + if let settingsAction = self.settingsAction { + queryItems.append(URLQueryItem(name: "x_settings_action", value: settingsAction.rawValue)) + } + if self.isSSOEnabled == false { // For backward compatibility // If the developer updates the SDK but not the server diff --git a/Sources/Authgear.swift b/Sources/Authgear.swift index 770bb771..8b7172a1 100644 --- a/Sources/Authgear.swift +++ b/Sources/Authgear.swift @@ -43,7 +43,8 @@ struct AuthenticateOptions { idTokenHint: nil, maxAge: nil, wechatRedirectURI: self.wechatRedirectURI, - page: self.page + page: self.page, + settingsAction: nil ) } } @@ -79,7 +80,8 @@ struct ReauthenticateOptions { idTokenHint: idTokenHint, maxAge: self.maxAge ?? 0, wechatRedirectURI: self.wechatRedirectURI, - page: nil + page: nil, + settingsAction: nil ) } } @@ -203,10 +205,13 @@ public enum AuthenticationPage: String { public enum SettingsPage: String { case settings = "/settings" - case changePassword = "/settings/change_password" case identity = "/settings/identities" } +public enum SettingsAction: String { + case changePassword = "change_password" +} + public enum SessionStateChangeReason: String { case noToken = "NO_TOKEN" case foundToken = "FOUND_TOKEN" @@ -243,7 +248,7 @@ public class Authgear { private static let ExpireInPercentage = 0.9 static let CodeChallengeMethod = "S256" - static let SDKRedirectURI = "authgearsdk://host/path" + static let SDKRedirectURI = "nocallback://host/path" let name: String let clientId: String @@ -976,6 +981,10 @@ public class Authgear { redirectURI: String, uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, + settingsAction: SettingsAction? = nil, + idTokenHint: String? = nil, + responseType: ResponseType = ResponseType.none, + verifier: CodeVerifier? = nil, wechatRedirectURI: String? = nil, handler: URLCompletionHandler? ) { @@ -1001,7 +1010,7 @@ public class Authgear { let endpoint = try self.buildAuthorizationURL(request: OIDCAuthenticationRequest( redirectURI: redirectURI, - responseType: "none", + responseType: responseType.rawValue, scope: ["openid", "offline_access", "https://authgear.com/scopes/full-access"], isSSOEnabled: self.isSSOEnabled, state: nil, @@ -1010,11 +1019,12 @@ public class Authgear { loginHint: loginHint, uiLocales: uiLocales, colorScheme: colorScheme, - idTokenHint: nil, + idTokenHint: idTokenHint, maxAge: nil, wechatRedirectURI: wechatRedirectURI, - page: nil - ), verifier: nil) + page: nil, + settingsAction: settingsAction + ), verifier: verifier) handler?(.success(endpoint)) } catch { @@ -1027,7 +1037,6 @@ public class Authgear { path: String, uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, - closeOnSuccess: Bool? = false, handler: URLCompletionHandler? ) { let handler = handler.map { h in self.withMainQueueHandler(h) } @@ -1048,9 +1057,6 @@ public class Authgear { if let colorScheme = colorScheme { queryItems.append(URLQueryItem(name: "x_color_scheme", value: colorScheme.rawValue)) } - if closeOnSuccess == true { - queryItems.append(URLQueryItem(name: "redirect_uri", value: Authgear.SDKRedirectURI)) - } urlComponents.queryItems = queryItems let redirectURI = urlComponents.url! self.generateURL(redirectURI: redirectURI.absoluteString) { generatedResult in @@ -1071,16 +1077,14 @@ public class Authgear { uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, wechatRedirectURI: String? = nil, - handler: VoidCompletionHandler? = nil, - closeOnSuccess: Bool? = false + handler: VoidCompletionHandler? = nil ) { let handler = handler.map { h in withMainQueueHandler(h) } self.generateAuthgearURL( path: path, uiLocales: uiLocales, - colorScheme: colorScheme, - closeOnSuccess: closeOnSuccess + colorScheme: colorScheme ) { [weak self] result in guard let self = self else { return } @@ -1126,15 +1130,156 @@ public class Authgear { page: SettingsPage, uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, - wechatRedirectURI: String? = nil, - closeOnSuccess: Bool? = false + wechatRedirectURI: String? = nil ) { openURL( path: page.rawValue, uiLocales: uiLocales, colorScheme: colorScheme, + wechatRedirectURI: wechatRedirectURI + ) + } + + private func openSettingsAction( + action: SettingsAction, + uiLocales: [String]? = nil, + colorScheme: ColorScheme? = nil, + wechatRedirectURI: String? = nil, + redirectURI: String, + handler: VoidCompletionHandler? = nil + ) { + let verifier = CodeVerifier() + let this = self + + self.refreshIDToken(handler: { result in + switch result { + case .success: + self.generateURL( + redirectURI: redirectURI, + uiLocales: uiLocales, + colorScheme: colorScheme, + settingsAction: action, + idTokenHint: self.idTokenHint, + responseType: ResponseType.settingsAction, + verifier: verifier + ) { generatedResult in + switch generatedResult { + case let .failure(err): + handler?(.failure(err)) + case let .success(url): + // For opening setting page, sdk will not know when user end + // the setting page. + // So we cannot unregister the wechat uri in this case + // It is fine to not unresgister it, as everytime we open a + // new authorize section (authorize or setting page) + // registerCurrentWeChatRedirectURI will be called and overwrite + // previous registered wechatRedirectURI + self.registerCurrentWechatRedirectURI(uri: wechatRedirectURI) + + self.uiImplementation.openAuthorizationURL( + url: url, + redirectURI: URL(string: Authgear.SDKRedirectURI)!, + // prefersEphemeralWebBrowserSession is true so that + // the alert dialog is never prompted and + // the app session token cookie is forgotten when the webview is closed. + shareCookiesWithDeviceBrowser: true + ) { result in + self.unregisterCurrentWechatRedirectURI() + switch result { + case let .success(url): + this.finishSettingsAction(url: url, redirectURI: redirectURI, verifier: verifier) { result in + switch (result) { + case .success: + handler?(result) + case let .failure(error): + handler?(.failure(wrapError(error: error))) + } + } + case let .failure(error): + handler?(.failure(wrapError(error: error))) + } + } + } + } + case let .failure(error): + handler?(.failure(wrapError(error: error))) + } + }) + } + + func finishSettingsAction( + url: URL, + redirectURI: String, + verifier: CodeVerifier, + handler: VoidCompletionHandler + ) { + let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)! + let params = urlComponents.queryParams + + if let errorParams = params["error"] { + if errorParams == "cancel" { + return handler( + .failure(AuthgearError.cancel) + ) + } + return handler( + .failure(AuthgearError.oauthError( + OAuthError( + error: errorParams, + errorDescription: params["error_description"], + errorUri: params["error_uri"] + ) + )) + ) + } + + guard let code = params["code"] else { + return handler( + .failure(AuthgearError.oauthError( + OAuthError( + error: "invalid_request", + errorDescription: "Missing parameter: code", + errorUri: nil + ) + )) + ) + } + + do { + _ = try apiClient.syncRequestOIDCToken( + grantType: GrantType.settingsAction, + clientId: clientId, + deviceInfo: getDeviceInfo(), + redirectURI: redirectURI, + code: code, + codeVerifier: verifier.value, + codeChallenge: nil, + codeChallengeMethod: nil, + refreshToken: nil, + jwt: nil, + accessToken: nil, + xApp2AppDeviceKeyJwt: nil + ) + handler(.success(())) + } catch { + return handler(.failure(wrapError(error: error))) + } + } + + public func changePassword( + uiLocales: [String]? = nil, + colorScheme: ColorScheme? = nil, + wechatRedirectURI: String? = nil, + redirectURI: String, + handler: VoidCompletionHandler? = nil + ) { + openSettingsAction( + action: .changePassword, + uiLocales: uiLocales, + colorScheme: colorScheme, wechatRedirectURI: wechatRedirectURI, - closeOnSuccess: closeOnSuccess + redirectURI: redirectURI, + handler: handler ) } diff --git a/example/ios_example/App.swift b/example/ios_example/App.swift index 75d5968e..1a434eee 100644 --- a/example/ios_example/App.swift +++ b/example/ios_example/App.swift @@ -3,6 +3,8 @@ import SwiftUI class App: ObservableObject { static let redirectURI = "com.authgear.example://host/path" + static let changePasswordRedirectURI = "com.authgear.example://host/after-changing-password" + static let app2appRedirectURI = "https://authgear-demo.pandawork.com/app2app/redirect" static let app2appAuthorizeEndpoint = "https://authgear-demo.pandawork.com/app2app/authorize" static let wechatUniversalLink = "https://authgear-demo.pandawork.com/wechat/" @@ -234,12 +236,18 @@ class App: ObservableObject { } func changePassword() { - container?.open( - page: .changePassword, + container?.changePassword( colorScheme: self.colorScheme, wechatRedirectURI: App.wechatRedirectURI, - closeOnSuccess: true - ) + redirectURI: App.changePasswordRedirectURI + ) { result in + switch result { + case .success: + self.successAlertMessage = "Changed password successfully" + case let .failure(error): + self.setError(error) + } + } } func promoteAnonymousUser() {