From 79e235037b4cf43077866bd6c6f9a0e4ab629088 Mon Sep 17 00:00:00 2001 From: Newman Chow Date: Thu, 22 Feb 2024 13:55:04 +0800 Subject: [PATCH 1/6] Close change password page on success #161 --- Sources/Authgear.swift | 23 +++++++++++++++-------- example/ios_example/App.swift | 9 +++++++++ example/ios_example/ContentView.swift | 6 ++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Sources/Authgear.swift b/Sources/Authgear.swift index b280ff05..381f72b1 100644 --- a/Sources/Authgear.swift +++ b/Sources/Authgear.swift @@ -206,6 +206,7 @@ public enum AuthenticationPage: String { public enum SettingsPage: String { case settings = "/settings" + case changePassword = "/settings/change_password" case identity = "/settings/identities" } @@ -245,6 +246,7 @@ public class Authgear { private static let ExpireInPercentage = 0.9 static let CodeChallengeMethod = "S256" + static let SDKRedirectURI = "authgearsdk://host/path" let name: String let clientId: String @@ -1028,6 +1030,7 @@ 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,6 +1051,9 @@ 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 @@ -1068,14 +1074,16 @@ public class Authgear { uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, wechatRedirectURI: String? = nil, - handler: VoidCompletionHandler? = nil + handler: VoidCompletionHandler? = nil, + closeOnSuccess: Bool? = false ) { let handler = handler.map { h in withMainQueueHandler(h) } self.generateAuthgearURL( path: path, uiLocales: uiLocales, - colorScheme: colorScheme + colorScheme: colorScheme, + closeOnSuccess: closeOnSuccess ) { [weak self] result in guard let self = self else { return } @@ -1095,9 +1103,7 @@ public class Authgear { self.uiImplementation.openAuthorizationURL( url: endpoint, - // Opening an arbitrary URL does not have a clear goal. - // So here we pass a placeholder redirect uri. - redirectURI: URL(string: "nocallback://host/path")!, + 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. @@ -1106,7 +1112,6 @@ public class Authgear { self.unregisterCurrentWechatRedirectURI() switch result { case .success: - // This branch is unreachable. handler?(.success(())) case let .failure(error): if case AuthgearError.cancel = error { @@ -1124,13 +1129,15 @@ public class Authgear { page: SettingsPage, uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, - wechatRedirectURI: String? = nil + wechatRedirectURI: String? = nil, + closeOnSuccess: Bool? = false ) { openURL( path: page.rawValue, uiLocales: uiLocales, colorScheme: colorScheme, - wechatRedirectURI: wechatRedirectURI + wechatRedirectURI: wechatRedirectURI, + closeOnSuccess: closeOnSuccess ) } diff --git a/example/ios_example/App.swift b/example/ios_example/App.swift index 9699f7cd..75d5968e 100644 --- a/example/ios_example/App.swift +++ b/example/ios_example/App.swift @@ -233,6 +233,15 @@ class App: ObservableObject { ) } + func changePassword() { + container?.open( + page: .changePassword, + colorScheme: self.colorScheme, + wechatRedirectURI: App.wechatRedirectURI, + closeOnSuccess: true + ) + } + func promoteAnonymousUser() { container?.promoteAnonymousUser( redirectURI: App.redirectURI, diff --git a/example/ios_example/ContentView.swift b/example/ios_example/ContentView.swift index bc44cb70..b285625b 100644 --- a/example/ios_example/ContentView.swift +++ b/example/ios_example/ContentView.swift @@ -272,6 +272,12 @@ struct ActionButtonList: View { ActionButton(text: "Open Setting") }.disabled(!configured || !loggedIn || isAnonymous) + Button(action: { + self.app.changePassword() + }) { + ActionButton(text: "Change Password") + }.disabled(!configured || !loggedIn || isAnonymous) + Button(action: { self.app.showAuthTime() }) { From 96d0e206ac73a4a1a85e410b2413102129379704 Mon Sep 17 00:00:00 2001 From: Newman Chow Date: Fri, 23 Feb 2024 22:33:41 +0800 Subject: [PATCH 2/6] 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 381f72b1..77fff3e3 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 ) } } @@ -206,10 +208,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" @@ -246,7 +251,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 @@ -979,6 +984,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? ) { @@ -1004,7 +1013,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, @@ -1013,11 +1022,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 { @@ -1030,7 +1040,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) } @@ -1051,9 +1060,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 @@ -1074,16 +1080,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 } @@ -1129,15 +1133,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() { From 0ca0f560e33a314710777c9895a179f066baf897 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 6 Mar 2024 14:54:08 +0800 Subject: [PATCH 3/6] Make responseType required --- Sources/Authgear.swift | 6 +++--- Sources/AuthgearExperimental.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Authgear.swift b/Sources/Authgear.swift index 77fff3e3..f95b57a3 100644 --- a/Sources/Authgear.swift +++ b/Sources/Authgear.swift @@ -982,11 +982,11 @@ public class Authgear { func generateURL( redirectURI: String, + responseType: ResponseType, uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, settingsAction: SettingsAction? = nil, idTokenHint: String? = nil, - responseType: ResponseType = ResponseType.none, verifier: CodeVerifier? = nil, wechatRedirectURI: String? = nil, handler: URLCompletionHandler? @@ -1062,7 +1062,7 @@ public class Authgear { } urlComponents.queryItems = queryItems let redirectURI = urlComponents.url! - self.generateURL(redirectURI: redirectURI.absoluteString) { generatedResult in + self.generateURL(redirectURI: redirectURI.absoluteString, responseType: .none) { generatedResult in switch generatedResult { case let .failure(err): handler?(.failure(err)) @@ -1159,11 +1159,11 @@ public class Authgear { case .success: self.generateURL( redirectURI: redirectURI, + responseType: .settingsAction, uiLocales: uiLocales, colorScheme: colorScheme, settingsAction: action, idTokenHint: self.idTokenHint, - responseType: ResponseType.settingsAction, verifier: verifier ) { generatedResult in switch generatedResult { diff --git a/Sources/AuthgearExperimental.swift b/Sources/AuthgearExperimental.swift index 0a522c6e..3085d562 100644 --- a/Sources/AuthgearExperimental.swift +++ b/Sources/AuthgearExperimental.swift @@ -10,7 +10,7 @@ public struct AuthgearExperimental { } public func generateURL(redirectURI: String, handler: URLCompletionHandler?) { - self.authgear.generateURL(redirectURI: redirectURI, handler: handler) + self.authgear.generateURL(redirectURI: redirectURI, responseType: .none, handler: handler) } public func createAuthenticateRequest( From 45fdff23d424e3cd92449b1c77f6715515e97eb7 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 6 Mar 2024 14:59:14 +0800 Subject: [PATCH 4/6] Fix redirectURI is not used --- Sources/Authgear.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Authgear.swift b/Sources/Authgear.swift index f95b57a3..727df798 100644 --- a/Sources/Authgear.swift +++ b/Sources/Authgear.swift @@ -251,7 +251,7 @@ public class Authgear { private static let ExpireInPercentage = 0.9 static let CodeChallengeMethod = "S256" - static let SDKRedirectURI = "nocallback://host/path" + private static let SDKRedirectURI = "nocallback://host/path" let name: String let clientId: String @@ -1144,11 +1144,11 @@ public class Authgear { } private func openSettingsAction( + redirectURI: String, action: SettingsAction, uiLocales: [String]? = nil, colorScheme: ColorScheme? = nil, wechatRedirectURI: String? = nil, - redirectURI: String, handler: VoidCompletionHandler? = nil ) { let verifier = CodeVerifier() @@ -1181,7 +1181,7 @@ public class Authgear { self.uiImplementation.openAuthorizationURL( url: url, - redirectURI: URL(string: Authgear.SDKRedirectURI)!, + redirectURI: URL(string: redirectURI)!, // prefersEphemeralWebBrowserSession is true so that // the alert dialog is never prompted and // the app session token cookie is forgotten when the webview is closed. @@ -1277,11 +1277,11 @@ public class Authgear { handler: VoidCompletionHandler? = nil ) { openSettingsAction( + redirectURI: redirectURI, action: .changePassword, uiLocales: uiLocales, colorScheme: colorScheme, wechatRedirectURI: wechatRedirectURI, - redirectURI: redirectURI, handler: handler ) } From ffa21e6c699cb0dcead9d7af326ca33525de988a Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 6 Mar 2024 15:01:29 +0800 Subject: [PATCH 5/6] Fix shareCookiesWithDeviceBrowser is inverted --- Sources/Authgear.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Authgear.swift b/Sources/Authgear.swift index 727df798..ee8642ba 100644 --- a/Sources/Authgear.swift +++ b/Sources/Authgear.swift @@ -1108,10 +1108,10 @@ public class Authgear { self.uiImplementation.openAuthorizationURL( url: endpoint, redirectURI: URL(string: Authgear.SDKRedirectURI)!, - // prefersEphemeralWebBrowserSession is true so that - // the alert dialog is never prompted and + // shareCookiesWithDeviceBrowser is false so that + // ASWebAuthenticationSession alert dialog is never prompted and // the app session token cookie is forgotten when the webview is closed. - shareCookiesWithDeviceBrowser: true + shareCookiesWithDeviceBrowser: false ) { result in self.unregisterCurrentWechatRedirectURI() switch result { @@ -1182,10 +1182,10 @@ public class Authgear { self.uiImplementation.openAuthorizationURL( url: url, redirectURI: URL(string: redirectURI)!, - // prefersEphemeralWebBrowserSession is true so that - // the alert dialog is never prompted and + // shareCookiesWithDeviceBrowser is false so that + // ASWebAuthenticationSession alert dialog is never prompted and // the app session token cookie is forgotten when the webview is closed. - shareCookiesWithDeviceBrowser: true + shareCookiesWithDeviceBrowser: false ) { result in self.unregisterCurrentWechatRedirectURI() switch result { From 82a8f81d510627bea8e0fea58963f840adaaeb0f Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 6 Mar 2024 15:02:19 +0800 Subject: [PATCH 6/6] Remove unnecessary redirect URI --- example/ios_example/App.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example/ios_example/App.swift b/example/ios_example/App.swift index 1a434eee..33258662 100644 --- a/example/ios_example/App.swift +++ b/example/ios_example/App.swift @@ -3,7 +3,6 @@ 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" @@ -239,7 +238,7 @@ class App: ObservableObject { container?.changePassword( colorScheme: self.colorScheme, wechatRedirectURI: App.wechatRedirectURI, - redirectURI: App.changePasswordRedirectURI + redirectURI: App.redirectURI ) { result in switch result { case .success: