From 68efe51c96908138ac4c0eb7036c445362e69fa3 Mon Sep 17 00:00:00 2001 From: Maximilian Kucher Date: Fri, 8 Aug 2025 14:52:52 +0200 Subject: [PATCH 1/7] add mtls support for ios --- ios/Plugin.xcodeproj/project.pbxproj | 6 +++ ios/Plugin/GenericOAuth2Plugin.swift | 71 +++++++++++++++++++++++++ ios/Plugin/URLSessionMTLSDelegate.swift | 28 ++++++++++ src/definitions.ts | 11 ++++ 4 files changed, 116 insertions(+) create mode 100644 ios/Plugin/URLSessionMTLSDelegate.swift diff --git a/ios/Plugin.xcodeproj/project.pbxproj b/ios/Plugin.xcodeproj/project.pbxproj index cde66770..505dde72 100644 --- a/ios/Plugin.xcodeproj/project.pbxproj +++ b/ios/Plugin.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; + 22BF03F12E44D2D30092F0FA /* URLSessionMTLSDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BF03F02E44D2D30092F0FA /* URLSessionMTLSDelegate.swift */; }; + 22BF03F22E44D2D30092F0FA /* URLSessionMTLSDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BF03F02E44D2D30092F0FA /* URLSessionMTLSDelegate.swift */; }; 451C6E972BE3BF4400D9577D /* OAuth2CustomHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451C6E962BE3BF4400D9577D /* OAuth2CustomHandler.swift */; }; 451C6E992BE3BF7200D9577D /* OAuth2SafariDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451C6E982BE3BF7200D9577D /* OAuth2SafariDelegate.swift */; }; 451C6E9B2BE3BF9F00D9577D /* GenericOAuth2Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451C6E9A2BE3BF9F00D9577D /* GenericOAuth2Plugin.swift */; }; @@ -31,6 +33,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 22BF03F02E44D2D30092F0FA /* URLSessionMTLSDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionMTLSDelegate.swift; sourceTree = ""; }; 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 451C6E962BE3BF4400D9577D /* OAuth2CustomHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CustomHandler.swift; sourceTree = ""; }; 451C6E982BE3BF7200D9577D /* OAuth2SafariDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2SafariDelegate.swift; sourceTree = ""; }; @@ -96,6 +99,7 @@ 50ADFF8A201F53D600D50D53 /* Plugin */ = { isa = PBXGroup; children = ( + 22BF03F02E44D2D30092F0FA /* URLSessionMTLSDelegate.swift */, 451C6E982BE3BF7200D9577D /* OAuth2SafariDelegate.swift */, 451C6E962BE3BF4400D9577D /* OAuth2CustomHandler.swift */, 50E1A94720377CB70090CE1A /* GenericOAuth2Plugin.swift */, @@ -315,6 +319,7 @@ buildActionMask = 2147483647; files = ( 451C6E972BE3BF4400D9577D /* OAuth2CustomHandler.swift in Sources */, + 22BF03F12E44D2D30092F0FA /* URLSessionMTLSDelegate.swift in Sources */, 50E1A94820377CB70090CE1A /* GenericOAuth2Plugin.swift in Sources */, 50ADFFA82020EE4F00D50D53 /* GenericOAuth2Plugin.m in Sources */, 451C6E992BE3BF7200D9577D /* OAuth2SafariDelegate.swift in Sources */, @@ -327,6 +332,7 @@ buildActionMask = 2147483647; files = ( 50ADFF97201F53D600D50D53 /* GenericOAuth2Tests.swift in Sources */, + 22BF03F22E44D2D30092F0FA /* URLSessionMTLSDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Plugin/GenericOAuth2Plugin.swift b/ios/Plugin/GenericOAuth2Plugin.swift index 633e7c18..0fab9170 100644 --- a/ios/Plugin/GenericOAuth2Plugin.swift +++ b/ios/Plugin/GenericOAuth2Plugin.swift @@ -3,6 +3,7 @@ import Capacitor import OAuthSwift import CommonCrypto import AuthenticationServices +import Security typealias JSObject = [String: Any] @@ -14,6 +15,7 @@ typealias JSObject = [String: Any] public class GenericOAuth2Plugin: CAPPlugin { var savedPluginCall: CAPPluginCall? + var mtlsSessionDelegate: URLSessionMTLSDelegate? let JSON_KEY_ACCESS_TOKEN = "access_token" let JSON_KEY_AUTHORIZATION_RESPONSE = "authorization_response" @@ -40,6 +42,10 @@ public class GenericOAuth2Plugin: CAPPlugin { let PARAM_LOGOUT_URL = "logoutUrl" let PARAM_LOGS_ENABLED = "logsEnabled" + // mTLS params + let PARAM_RAW_PKCS = "rawPkcs" + let PARAM_PKCS_PASSWORD = "pkcsPassword" + let ERR_GENERAL = "ERR_GENERAL" // authenticate param validation @@ -58,6 +64,8 @@ public class GenericOAuth2Plugin: CAPPlugin { let ERR_NO_AUTHORIZATION_CODE = "ERR_NO_AUTHORIZATION_CODE" let ERR_AUTHORIZATION_FAILED = "ERR_AUTHORIZATION_FAILED" + let ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED = "ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED" + struct SharedConstants { static let ERR_USER_CANCELLED = "USER_CANCELLED" } @@ -123,6 +131,9 @@ public class GenericOAuth2Plugin: CAPPlugin { return } + let rawPkcsString = getOverwritableString(call, PARAM_RAW_PKCS) ?? "" + let pkcsPassword = getOverwritableString(call, PARAM_PKCS_PASSWORD) ?? "" + let oauthSwift = OAuth2Swift( consumerKey: appId, consumerSecret: "", // never ever store the app secret on client! @@ -131,6 +142,17 @@ public class GenericOAuth2Plugin: CAPPlugin { responseType: "code" ) + if (rawPkcsString.count > 0) { + if self.mtlsSessionDelegate == nil { + self.mtlsSessionDelegate = self.createMTLSSessionDelegate(rawPkcsString: rawPkcsString, pkcsPassword: pkcsPassword, call: call) + } + if self.mtlsSessionDelegate == nil { + call.reject(self.ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED) + return + } + oauthSwift.client.sessionFactory.delegate = self.mtlsSessionDelegate + } + self.oauthSwift = oauthSwift let scope = getOverwritableString(call, PARAM_SCOPE) ?? nil @@ -192,6 +214,9 @@ public class GenericOAuth2Plugin: CAPPlugin { // #71 self.oauth2SafariDelegate = OAuth2SafariDelegate(call) + let rawPkcsString = getOverwritableString(call, PARAM_RAW_PKCS) ?? "" + let pkcsPassword = getOverwritableString(call, PARAM_PKCS_PASSWORD) ?? "" + // ######### Custom Handler ######## if let handlerClassName = getString(call, PARAM_CUSTOM_HANDLER_CLASS) { @@ -207,6 +232,15 @@ public class GenericOAuth2Plugin: CAPPlugin { oauthTokenSecret: "", version: OAuthSwiftCredential.Version.oauth2) + if (rawPkcsString.count > 0) { + self.mtlsSessionDelegate = self.createMTLSSessionDelegate(rawPkcsString: rawPkcsString, pkcsPassword: pkcsPassword, call: call) + if self.mtlsSessionDelegate == nil { + call.reject(self.ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED) + return + } + client.sessionFactory.delegate = self.mtlsSessionDelegate + } + client.get(resourceUrl!) { result in switch result { case .success(let response): @@ -279,10 +313,25 @@ public class GenericOAuth2Plugin: CAPPlugin { ) } + if (rawPkcsString.count > 0) { + if self.mtlsSessionDelegate == nil { + self.mtlsSessionDelegate = self.createMTLSSessionDelegate(rawPkcsString: rawPkcsString, pkcsPassword: pkcsPassword, call: call) + } + if self.mtlsSessionDelegate == nil { + call.reject(self.ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED) + return + } + oauthSwift.client.sessionFactory.delegate = self.mtlsSessionDelegate + } + let urlHandler = SafariURLHandler(viewController: (bridge?.viewController)!, oauthSwift: oauthSwift) // if the user touches "done" in safari without entering the credentials the USER_CANCELLED error is sent #71 urlHandler.delegate = self.oauth2SafariDelegate oauthSwift.authorizeURLHandler = urlHandler + + //TODO: remove after development + OAuthSwift.setLogLevel(OAuthLogLevel.trace) + self.oauthSwift = oauthSwift // additional parameters #18 @@ -550,6 +599,28 @@ public class GenericOAuth2Plugin: CAPPlugin { return randomString } + private func createMTLSSessionDelegate(rawPkcsString: String, pkcsPassword: String, call: CAPPluginCall) -> URLSessionMTLSDelegate? { + let pkcsData = Data(base64Encoded: rawPkcsString) ?? Data() + + var importedItems: CFArray? + let options = [kSecImportExportPassphrase as String: pkcsPassword] + let importStatus = SecPKCS12Import(pkcsData as CFData, options as CFDictionary, &importedItems) + + guard importStatus == errSecSuccess else { + self.log("Failed to import pkcs file. Error code: \(importStatus)") + return nil + } + + guard let itemsArray = importedItems as? [[String: AnyObject]], + let firstItem = itemsArray.first, + let secIdentity = firstItem[kSecImportItemIdentity as String] as! SecIdentity? + else { + self.log("Failed to extract identity from the pkcs file") + return nil + } + + return URLSessionMTLSDelegate(secIdentity: secIdentity) + } } // see https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce diff --git a/ios/Plugin/URLSessionMTLSDelegate.swift b/ios/Plugin/URLSessionMTLSDelegate.swift new file mode 100644 index 00000000..3326c59a --- /dev/null +++ b/ios/Plugin/URLSessionMTLSDelegate.swift @@ -0,0 +1,28 @@ +import Foundation +import Security + +public class URLSessionMTLSDelegate: NSObject, URLSessionDelegate { + private let secIdentity: SecIdentity + + public init(secIdentity: SecIdentity) { + self.secIdentity = secIdentity + super.init() + } + + public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let serverCertificate = challenge.protectionSpace.serverTrust { + let credential = URLCredential(trust: serverCertificate) + completionHandler(.useCredential, credential) + } + else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { + let credential = URLCredential(identity: secIdentity, certificates: nil, persistence: .forSession) + completionHandler(.useCredential, credential) + } else { + completionHandler(.performDefaultHandling, nil) + } + } +} \ No newline at end of file diff --git a/src/definitions.ts b/src/definitions.ts index 20efbb91..dbfa860b 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -124,6 +124,17 @@ export interface OAuth2AuthenticateBaseOptions { * @since 3.0.0 */ additionalResourceHeaders?: { [key: string]: string }; + + /** + * Client certificate data for mTLS authentication + * Raw PKCS data from a .p12 or .pfx file as a base64 encoded string + */ + rawPkcs?: string; + + /** + * Password for the client certificate + */ + pkcsPassword?: string; } export interface OAuth2AuthenticateOptions From d91c1ecc2a39886cad6b85e2ff7a2be00739fb7b Mon Sep 17 00:00:00 2001 From: Maximilian Kucher Date: Thu, 28 Aug 2025 08:56:44 +0200 Subject: [PATCH 2/7] definitions for token refresh api and move var to better suiting place --- ios/Plugin/GenericOAuth2Plugin.swift | 2 +- src/definitions.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ios/Plugin/GenericOAuth2Plugin.swift b/ios/Plugin/GenericOAuth2Plugin.swift index 0fab9170..ec14300a 100644 --- a/ios/Plugin/GenericOAuth2Plugin.swift +++ b/ios/Plugin/GenericOAuth2Plugin.swift @@ -15,7 +15,6 @@ typealias JSObject = [String: Any] public class GenericOAuth2Plugin: CAPPlugin { var savedPluginCall: CAPPluginCall? - var mtlsSessionDelegate: URLSessionMTLSDelegate? let JSON_KEY_ACCESS_TOKEN = "access_token" let JSON_KEY_AUTHORIZATION_RESPONSE = "authorization_response" @@ -74,6 +73,7 @@ public class GenericOAuth2Plugin: CAPPlugin { var oauth2SafariDelegate: OAuth2SafariDelegate? var handlerClasses = [String: OAuth2CustomHandler.Type]() var handlerInstances = [String: OAuth2CustomHandler]() + var mtlsSessionDelegate: URLSessionMTLSDelegate? func registerHandlers() { let classCount = objc_getClassList(nil, 0) diff --git a/src/definitions.ts b/src/definitions.ts index dbfa860b..71a6ac4f 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -55,6 +55,15 @@ export interface OAuth2RefreshTokenOptions { * A space-delimited list of permissions that identify the resources that your application could access on the user's behalf. */ scope?: string; + /** + * Client certificate data for mTLS authentication + * Raw PKCS data from a .p12 or .pfx file as a base64 encoded string + */ + rawPkcs?: string; + /** + * Password for the client certificate + */ + pkcsPassword?: string; } export interface OAuth2AuthenticateBaseOptions { From 49a84fb5f54e7bf124c0e18b7bf95a9cf2ca2891 Mon Sep 17 00:00:00 2001 From: Maximilian Kucher Date: Thu, 28 Aug 2025 08:57:04 +0200 Subject: [PATCH 3/7] add .vscode to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 384a9e3a..ad201bac 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,9 @@ captures *.iml .idea +# VSCode files +.vscode + # Keystore files # Uncomment the following line if you do not want to check your keystore files in. #*.jks From 80b6c5a90bd1d55e947daeae80e629d7bcaf0f0f Mon Sep 17 00:00:00 2001 From: Maximilian Kucher Date: Fri, 29 Aug 2025 11:32:16 +0200 Subject: [PATCH 4/7] add mtls support for android --- .../genericoauth2/GenericOAuth2Plugin.java | 30 +++++++++- .../community/genericoauth2/MTLSHelper.java | 56 +++++++++++++++++++ .../genericoauth2/OAuth2Options.java | 19 +++++++ .../OAuth2RefreshTokenOptions.java | 18 ++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 android/src/main/java/com/getcapacitor/community/genericoauth2/MTLSHelper.java diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java index eb2c9fd9..bc297ecb 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java @@ -28,7 +28,7 @@ import net.openid.appauth.TokenRequest; import net.openid.appauth.TokenResponse; import org.json.JSONException; - +import com.getcapacitor.community.genericoauth2.MTLSHelper; @CapacitorPlugin(name = "GenericOAuth2") public class GenericOAuth2Plugin extends Plugin { @@ -62,6 +62,10 @@ public class GenericOAuth2Plugin extends Plugin { private static final String PARAM_LOGOUT_URL = "logoutUrl"; private static final String PARAM_ID_TOKEN = "id_token"; + // mTLS params + private static final String PARAM_RAW_PKCS = "rawPkcs"; + private static final String PARAM_PKCS_PASSWORD = "pkcsPassword"; + private static final String USER_CANCELLED = "USER_CANCELLED"; private static final String ERR_PARAM_NO_APP_ID = "ERR_PARAM_NO_APP_ID"; @@ -84,6 +88,8 @@ public class GenericOAuth2Plugin extends Plugin { private static final String ERR_STATES_NOT_MATCH = "ERR_STATES_NOT_MATCH"; private static final String ERR_NO_AUTHORIZATION_CODE = "ERR_NO_AUTHORIZATION_CODE"; + private static final String ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED = "ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED"; + private OAuth2Options oauth2Options; private AuthorizationService authService; private AuthState authState; @@ -111,6 +117,14 @@ public void refreshToken(final PluginCall call) { return; } + if (oAuth2RefreshTokenOptions.getRawPkcs() != null && !oAuth2RefreshTokenOptions.getRawPkcs().isEmpty()) { + try { + MTLSHelper.configureMTLS(getContext(), oAuth2RefreshTokenOptions.getRawPkcs(), oAuth2RefreshTokenOptions.getPkcsPassword()); + } catch (Exception e) { + call.reject(ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED, e); + } + } + this.authService = new AuthorizationService(getContext()); AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration( @@ -271,6 +285,14 @@ public void onError(Exception error) { AuthorizationRequest req = builder.build(); + if (oauth2Options.getRawPkcs() != null && !oauth2Options.getRawPkcs().isEmpty()) { + try { + MTLSHelper.configureMTLS(getContext(), oauth2Options.getRawPkcs(), oauth2Options.getPkcsPassword()); + } catch (Exception e) { + call.reject(ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED, e); + } + } + this.authService = new AuthorizationService(getContext()); try { Intent authIntent = this.authService.getAuthorizationRequestIntent(req); @@ -511,6 +533,8 @@ OAuth2Options buildAuthenticateOptions(JSObject callData) { if (o.isPkceEnabled()) { o.setPkceCodeVerifier(ConfigUtils.getRandomString(64)); } + o.setRawPkcs(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RAW_PKCS))); + o.setPkcsPassword(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_PKCS_PASSWORD))); o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE))); o.setState(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_STATE))); @@ -556,6 +580,10 @@ OAuth2RefreshTokenOptions buildRefreshTokenOptions(JSObject callData) { ); o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE))); o.setRefreshToken(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REFRESH_TOKEN))); + + // mTLS + o.setRawPkcs(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RAW_PKCS))); + o.setPkcsPassword(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_PKCS_PASSWORD))); return o; } diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/MTLSHelper.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/MTLSHelper.java new file mode 100644 index 00000000..21075982 --- /dev/null +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/MTLSHelper.java @@ -0,0 +1,56 @@ +package com.getcapacitor.community.genericoauth2; + +import android.content.Context; +import android.util.Log; +import java.io.ByteArrayInputStream; +import java.security.KeyStore; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +public class MTLSHelper { + private static final String TAG = "MTLSHelper"; + private static SSLContext sslContext; + + public static void configureMTLS(Context context, String rawPkcs, String pkcsPassword) { + if (rawPkcs == null || rawPkcs.isEmpty()) { + Log.d(TAG, "No certificate data provided, skipping mTLS configuration"); + return; + } + + try { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + byte[] pkcsData = android.util.Base64.decode(rawPkcs, android.util.Base64.DEFAULT); + keyStore.load(new ByteArrayInputStream(pkcsData), pkcsPassword.toCharArray()); + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, pkcsPassword.toCharArray()); + + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), null, null); + + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + + Log.d(TAG, "mTLS client certificate configured successfully"); + } catch (Exception e) { + Log.e(TAG, "Failed to configure mTLS", e); + throw new RuntimeException("mTLS configuration failed", e); + } + } + + public static SSLContext getSSLContext() { + return sslContext; + } + + public static void resetSSLContext() { + try { + SSLContext defaultContext = SSLContext.getInstance("TLS"); + defaultContext.init(null, null, null); + HttpsURLConnection.setDefaultSSLSocketFactory(defaultContext.getSocketFactory()); + sslContext = null; + Log.d(TAG, "SSL context reset to default"); + } catch (Exception e) { + Log.e(TAG, "Failed to reset SSL context", e); + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java index 9bd064a0..eefc7095 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java @@ -28,6 +28,9 @@ public class OAuth2Options { private boolean handleResultOnNewIntent; private boolean handleResultOnActivityResult = true; + private String rawPkcs; + private String pkcsPassword; + private String display; private String loginHint; private String prompt; @@ -216,4 +219,20 @@ public void addAdditionalResourceHeader(String key, String value) { public String getLogoutUrl() { return logoutUrl; } + + public String getRawPkcs() { + return rawPkcs; + } + + public void setRawPkcs(String rawPkcs) { + this.rawPkcs = rawPkcs; + } + + public String getPkcsPassword() { + return pkcsPassword; + } + + public void setPkcsPassword(String pkcsPassword) { + this.pkcsPassword = pkcsPassword; + } } diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java index 71040e2d..f865a0c8 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java @@ -6,6 +6,8 @@ public class OAuth2RefreshTokenOptions { private String accessTokenEndpoint; private String refreshToken; private String scope; + private String rawPkcs; + private String pkcsPassword; public String getAppId() { return appId; @@ -38,4 +40,20 @@ public String getScope() { public void setScope(String scope) { this.scope = scope; } + + public String getRawPkcs() { + return rawPkcs; + } + + public void setRawPkcs(String rawPkcs) { + this.rawPkcs = rawPkcs; + } + + public String getPkcsPassword() { + return pkcsPassword; + } + + public void setPkcsPassword(String pkcsPassword) { + this.pkcsPassword = pkcsPassword; + } } From 9e144f7308954b665007a5ca0cd169355d0e085a Mon Sep 17 00:00:00 2001 From: Maximilian Kucher Date: Fri, 29 Aug 2025 11:32:39 +0200 Subject: [PATCH 5/7] add new parameters and error descriptions to readme --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b26bde0d..2427b4da 100644 --- a/README.md +++ b/README.md @@ -213,13 +213,17 @@ These parameters are overrideable in every platform | customHandlerClass | | | Provide a class name implementing `com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler` | | | handleResultOnNewIntent | `false` | | Alternative to handle the activity result. The `onNewIntent` method is only call if the App was killed while logging in. | | | handleResultOnActivityResult | `true` | | | | +| rawPkcs | | | Provide raw PKCS data from a .p12 or .pfx file as a base64 encoded string for mTLS authentication. | | +| pkcsPassword | | | Provide an optional password for the PKCS data if it is password secured. | | **Platform iOS** -| parameter | default | required | description | since | -|--------------------|---------|----------|------------------------------------------------------------------------------------------------|-------| -| customHandlerClass | | | Provide a class name implementing `CapacitorCommunityGenericOauth2.OAuth2CustomHandler` | | -| siwaUseScope | | | SiWA default scope is `name email` if you want to use the configured one set this param `true` | 2.1.0 | +| parameter | default | required | description | since | +|--------------------|---------|----------|----------------------------------------------------------------------------------------------------|-------| +| customHandlerClass | | | Provide a class name implementing `CapacitorCommunityGenericOauth2.OAuth2CustomHandler` | | +| siwaUseScope | | | SiWA default scope is `name email` if you want to use the configured one set this param `true` | 2.1.0 | +| rawPkcs | | | Provide raw PKCS data from a .p12 or .pfx file as a base64 encoded string for mTLS authentication. | | +| pkcsPassword | | | Provide an optional password for the PKCS data if it is password secured. | | #### logout() @@ -254,6 +258,7 @@ See [Issue #97](https://github.com/capacitor-community/generic-oauth2/issues/97) - ERR_ANDROID_NO_BROWSER ... No suitable browser could be found! (Android) - ERR_ANDROID_RESULT_NULL ... The auth result is null. The intent in the ActivityResult is null. This might be a valid state but make sure you configured Android part correctly! See [Platform Android](#platform-android) +- ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED ... Importing the provided pkcs data was not successful. Most likely the string you provided is in the wrong format. A base64 encoded string of the raw pkcs data file is expected. Also check if the file is protected by a password and if so, if you provided the correct password accordingly - ERR_GENERAL ... A unspecific error. Check the logs to see want exactly happened. (web, android, ios) #### refreshToken() @@ -263,6 +268,7 @@ See [Issue #97](https://github.com/capacitor-community/generic-oauth2/issues/97) authenticate it is optional. (android, ios) - ERR_PARAM_NO_REFRESH_TOKEN ... The refresh token is missing. (android, ios) - ERR_NO_ACCESS_TOKEN ... No access_token found. (web, android) +- ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED ... Importing the provided pkcs data was not successful. Most likely the string you provided is in the wrong format. A base64 encoded string of the raw pkcs data file is expected. Also check if the file is protected by a password and if so, if you provided the correct password accordingly - ERR_GENERAL ... A unspecific error. Check the logs to see want exactly happened. (android, ios) ## Platform: Web/PWA From a4db48155f173933150b7e40c57062cedb4c93d1 Mon Sep 17 00:00:00 2001 From: Maximilian Kucher Date: Fri, 29 Aug 2025 11:42:56 +0200 Subject: [PATCH 6/7] configureMTLS before customHandler --- .../genericoauth2/GenericOAuth2Plugin.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java index bc297ecb..6f169903 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java @@ -170,6 +170,15 @@ public void authenticate(final PluginCall call) { this.callbackId = call.getCallbackId(); disposeAuthService(); oauth2Options = buildAuthenticateOptions(call.getData()); + + if (oauth2Options.getRawPkcs() != null && !oauth2Options.getRawPkcs().isEmpty()) { + try { + MTLSHelper.configureMTLS(getContext(), oauth2Options.getRawPkcs(), oauth2Options.getPkcsPassword()); + } catch (Exception e) { + call.reject(ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED, e); + } + } + if (oauth2Options.getCustomHandlerClass() != null) { if (oauth2Options.isLogsEnabled()) { Log.i(getLogTag(), "Entering custom handler: " + oauth2Options.getCustomHandlerClass().getClass().getName()); @@ -285,14 +294,6 @@ public void onError(Exception error) { AuthorizationRequest req = builder.build(); - if (oauth2Options.getRawPkcs() != null && !oauth2Options.getRawPkcs().isEmpty()) { - try { - MTLSHelper.configureMTLS(getContext(), oauth2Options.getRawPkcs(), oauth2Options.getPkcsPassword()); - } catch (Exception e) { - call.reject(ERR_MTLS_CLIENT_CERTIFICATE_IMPORT_FAILED, e); - } - } - this.authService = new AuthorizationService(getContext()); try { Intent authIntent = this.authService.getAuthorizationRequestIntent(req); From 331e2a7b76d5f7b22ddea6a923d6b9b80fec445e Mon Sep 17 00:00:00 2001 From: Maximilian Kucher Date: Tue, 2 Sep 2025 10:49:52 +0200 Subject: [PATCH 7/7] remove trace level logging --- ios/Plugin/GenericOAuth2Plugin.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/ios/Plugin/GenericOAuth2Plugin.swift b/ios/Plugin/GenericOAuth2Plugin.swift index ec14300a..92d1d88b 100644 --- a/ios/Plugin/GenericOAuth2Plugin.swift +++ b/ios/Plugin/GenericOAuth2Plugin.swift @@ -329,9 +329,6 @@ public class GenericOAuth2Plugin: CAPPlugin { urlHandler.delegate = self.oauth2SafariDelegate oauthSwift.authorizeURLHandler = urlHandler - //TODO: remove after development - OAuthSwift.setLogLevel(OAuthLogLevel.trace) - self.oauthSwift = oauthSwift // additional parameters #18