MEP is a system for client authentication and transaction authorization. MEPi SDK is a set of components that allow multiple mobile applications (provided by group companies and 3rd parties - partners) to use federated identification, authentication and authorization features of MEP. Various components of MEPi SDK provide a way to integrate to various components of MEP's backend components. Integrating application will use MEPi SDK to authenticate its users and authorize their operations inside the application.
This documentation describes the MEPi SDK provided by Monet+ and AHEAD iTec to developers of applications that use the services of MEP. This documentation will be subject to further changes as the MEP evolve. This documentation does not describe any business and organizational aspects of the “Integrating application onboarding process”.
MEPi SDK and CMi SDK builds upon concept of federated identity. Identity of a user, that was paired with instance of CASE mobile application. The identity could be used by another application on device - hence federated identity.
Integrating application can outsource features like identification, authentication and authorization to the CASE mobile application.
This approach has multiple business benefits:
With CASE mobile handling all communication with the FS ecosystem, other integrating applications do not need to bother with implementation details of the MEP FS backend, do not need to handle enrollment, activation and other processes related to the lifecycle of the activated mobile app, and - most importantly - do not need to synchronize their lifecycle with releases the security infrastructure.
Applications could use this identity by integrating so called CASE mobile integration SDK (CMi SDK). The CMi SDK was designed as the single point of contact between integrating application and the MEP FS ecosystem.
MEPi SDK adds another set of features to the set. In cases, where integrating application does not want to enforce usage of CASE mobile identity. Integrating application can take more responsibilities and (via MEPi SDK) communicate directly with MEP FS backend. See further description about supported scenarios for details.
To some extend, MEPi SDK could be considered as next generation of CMi SDK. A slight problem with that is that MEPi SDK also provides different features than CMi SDK did and those new features do not relate to CASE mobile in any way. More correct way would be to say that MEPi SDK contains CMi SDK.
Nevertheless, in general, non-technical discussions those two terms can be used as synonyms.
Component of MEPi SDK are published to online repositories. Integrating application can either use dependencies directly from repository or download binaries and use them as local dependencies.
Monet's public Nexus repository (credentials required) Setup Nexus maven repository in your project. Add to module's gradle file:
repositories {
maven {
credentials {
username "<your username>"
password "<your password>"
}
url "https://nexus3-public.monetplus.cz/repository/ahead-android-csob-release/"
}
}
Add the following to your app/build.gradle inside the dependencies section:
implementation("com.aheaditec.mepisdk:cmi-tp:$latest_version")
implementation("com.aheaditec.mepisdk:commons:$latest_version")
implementation("com.aheaditec.mepisdk:fsi:$latest_version")
implementation("com.aheaditec.mepisdk:mepi:$latest_version")
implementation 'com.aheaditec.utils:network:$latest_version'
implementation 'com.aheaditec.talsec:storage:$latest_version'
implementation 'com.aheaditec.talsec:ClientCertificates:$latest_version'
implementation 'com.aheaditec:functional:$latest_version'
// PKCS stuff
implementation 'com.madgag.spongycastle:pkix:1.51.0.0'
implementation 'com.madgag.spongycastle:core:1.51.0.0'
// JWS stuff
implementation "org.bitbucket.b_c:jose4j:0.6.5"
Monet's Github repository (SPM) Add following SPM dependency to Xcode project or Package.swift file:
{
"package": "MEPiSDK",
"repositoryURL": "[email protected]:monetplus/MEPiSDK_iOS.git",
"state": {
"branch": null,
"revision": "<<latest_revision>>",
"version": "<<latest_version>>"
}
},
Package is referencing built xcframeworks stored at Monet's Nexus
Deprecated: Add following line to Cartfile:
binary "https://raw.githubusercontent.com/monetplus/MEPiSDK_iOS/master/mepisdk_carthage/Mepi.json" ~> 0.1.0
MEPi SDK supports following scenarios:
Following sections provide detailed description of these scenarios.
MEPi component provides API for getting information required to decide which scenario should be presented to user by integrating application. This API provides information such as availability of CASE mobile, presence of TLS client certificate and its expiration date, availability of biometrics etc. Based on these data, integrating application will decide if:
MEPi.StatusFactory
val package = "cz.csob.smartklic"
val retriever = CaseMobileStatusRetriever(application, package)
val statusFactory = StatusFactory(retriever, application)
let cmBundleId = "cz.csob.smartklic"
let statusFactory = StatusFactory(
urlChecker: UIApplication.shared,
caseMobileBundleId: cmBundleId
)
MEPi.Status
val status = statusFactory.getStatus()
let statusResult = statusFactory.getStatus()
let status = statusResult.get()
retriever.retrieveEnhancedStatus(callback = object : CaseMobileStatusCallback {
override fun onCaseMobileStatusRetrieved(
result: Either<CaseMobileStatus, ErrorOutput>
) {
val enhancedStatus = result.getSuccessOrNull()
}
})
let factory = CaseMobileStatusFactory(
openUrlChecker: UIApplication.shared,
caseMobileBundleId: cmBundleId
)
let enhancedStatus: CaseMobileStatus =
factory.assembleCaseMobileStatusRetriever().retrieveEnhancedStatus()
val keyManagers: Either<Array<KeyManager>, ErrorOutput> =
statusFactory.getApplicationKeyManagers()
let identity: Result<SecIdentity?, ErrorOutput> =
statusFactory.getApplicationClientIdentity()
It is very common that applications require some form of logging in before a user can use the main functionality. Applications integrating MEPi SDK can rely on CASE identity of the user. Instead of forcing him to enter his credentials, an application can redirect him to CASE mobile application to confirm his new login operation. User is used to CASE mobile environment, so confirming login operation should not be a problem for him.
CASE mobile will handle the whole login process. If the user approves login operation, CASE mobile will get access token (or authentication code) from MEP and pass it back to calling application. User can also reject login operation. It that case, CASE mobile will return error code to calling application.
CMi's inter-app communication is based on app links and universal links. These links are a feature of recent versions of Android and iOS. They are enhancing for UX flows, where the user is required to switch context from one application to another.
For detailed description and information about setup, refer to the official documentation:
Essentially, these links are simple URLs. They have slightly different behavior on iOS and Android. On iOS, a request made by opening URL (that applications are listening to) causes OS to pass control to an application by calling a method in AppDelegate. Android can declare in the manifest which application component should be invoked by opening a particular URL.
This login scenario consists of 4 stages:
CMiTP
provides API for creating login requests and processing responses. It does not validate any part of URL except query parameters that are used to serialize data. Opening and receiving URL is the responsibility of an integrating application. CMiTP
does not restrict nor force (Android) application to use a particular component to receive URL requests from OS.
CASE mobile has its own links that will be used by integrating application and CMiTP
to send requests to CASE mobile. Integrating application must also setup its links to receive responses from CASE mobile (see links to official documentation above). Values of application's links are completely arbitrary, as long as they will not collide with values of CASE mobile and are registered in MEP system before using this login scenario. After registration, those links can be passed as redirect URIs.
CMiTP
's extensions on Swift.URL
(iOS) and Uri
(Android) with method appendingQuery()
, appendQuery()
or parseQuery()
val loginInput = ...//responseType “code”
val result: Either<Uri, ErrorOutput> = url.appendingQuery(input = loginInput)
val uri = result.getSuccessOrNull()!!
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
...//continue in CASE mobile
override fun onNewIntent(intent: Intent?) {
intent?.data?.let { uri ->
val networkCall = ...
val clientSecret = ...
uri.parseQuery(loginInput, networkCall).mapEither { output ->
output.exchangeForToken(loginInput, clientSecret, networkCall)
}
}
let loginInput = ...//responseType “code”
url.cmiProtectionMode = ...//applink vs custom scheme
let result: Result<URL, ErrorOutput> = url.appendingQuery(from: LoginInput)
//OR
let error: ErrorOutput? = url.appendQuery(from: loginInput)
...//continue in CASE mobile
let communicator = CommunicatorFactory.createForFederatedLogin() else { return }
let outputResult: Result<LoginOutput, ErrorOutput> =
url.parseQuery(loginInput: loginInput, federatedLoginCommunicator: communicator)
let loginOutput = try outputResult.get()
let exchanged: Result<LoginOutput, ErrorOutput> =
loginOutput.exchangeCodeForToken(clientSecret: String)
MEPiCommons.LoginInput
val responseType = ... // "code id_token", "token id_token" or "code"
val scopes = ... // requested scopes
val state = ... // custom value from application
val redirectUri = ... // uri to receive response
val clientId = ... // client id of application
val instanceId = ... // instanceId from activation
val oAuthRequest = OAuthRequest(
clientId = clientId,
redirectUri = redirectUri,
responseType = responseType,
scope = scope,
state = state
)
val claims = Claims.createCmiInstanceIdClaims(instanceId)
val openIdWithClaims = OpenIdConnectRequest(claims)
val loginInput = LoginInput(oAuthRequest, openIdWithClaims)
let responseType = ... // "code id_token", "token id_token" or "code"
let instanceId = ... // instanceId from activation,
let scopes = ... // requested scopes
let state = ... // custom value from application
let redirectUri = ... // uri to receive response
let clientId = ... // client id of application
let oAuthRequest = OAuthRequest(
state: state,
redirectUri: redirectUri,
clientId: clientid,
scope: scopes,
responseType: responseType
)
let claims = Claims(idTokenClaims: ["cmiInstanceId": ClaimContent(values: [instanceId])], userInfoClaims: nil)
let openIdConnectRequest = OpenIDConnectRequest(claims: claims)
let loginInput = LoginInput(oAuthRequest: oAuthRequest, openIDRequest: openIdConnectRequest)
MEPiCommons.LoginOutput
If CASE mobile is not available on the device, or integrating application does not want to enforce usage of CASE mobile identity, integrating application can present the user with a series of traditional forms to enable login by username, password, and SMS code. MEPI SDK will directly process credentials entered by user.
This login scenario consists of 3 stages:
FSi
provides API for processing data from such forms and for passing integrating application information required to show the next form in the scenario.
FSi.FSiLogin
val flCommunicator = NetworkCall("${serverUrl}/mep/fs/fl/")
val authCommunicator = NetworkCall("${serverUrl}/mep/fs/svc/authgtw/authn/")
val loginInput = ...
FSiLogin(flCommunicator, authCommunicator).start(
loginInput,
"language"
).mapEither { scenario ->
scenario.selectScenario("s_mobile_authn_un_pwd_sms")
}.mapEither { userNameAndPassword ->
userNameAndPassword.submit("userName", "password")
}.mapEither { sms ->
sms.submit("000-000-000")
}.mapEither { loginFinish ->
loginFinish.get()
}.biMap({ loginOutput -> }, { error -> }
guard let federatedLoginCommunicator = CommunicatorFactory.createForFederatedLogin() else { return }
guard let authGTWFLCommunicator = CommunicatorFactory.createForAuthGtwFl() else { return }
let fsiLogin = FSiLogin.init(federatedLoginCommunicator: federatedLoginCommunicator,
authGatewayFederatedLoginCommunicator: authGTWFLCommunicator)
let scenarioResult = fsiLogin.startLogin(loginInput: loginInput, language: "cs")
let scenario = try scenarioResult.get()
let requiredScenario = "s_mobile_authn_un_pwd_sms"
guard scenario.scenariosId.contains(requiredScenario) else { return }
let userNamePasswordResult = scenario.select(scenarioId: requiredScenario)
let userNameAndPassword = try userNamePasswordResult.get()
let smsResult = userNameAndPassword.submit(userName: userName, password: password)
let sms = try smsResult.get()
let loginFinishResult = sms.submit(sms: smsCode)
let loginFinish = try loginFinishResult.get()
let loginOutputResult = loginFinish.get()
let loginOutput = try loginOutputResult.get()
MEPi.BiometricLogin
MEPi.BiometricLoginUserAuthentication
(Android only)MEPi.BiometricPromptConfig
(Android only)MEPi.BiometricLoginChallenge
Android
val loginInput = ...
val authGtwCmNetworkCall = ... //with client cert
val flNetworkCall = …
val bioLogin = BiometricLogin(authGtwCmNetworkCall, flNetworkCall)
val bioUserAuthentication = bioLogin.getChallenge(loginInput).getSuccessOrNull()!!
val challenge = ... // authenticate user to unlock biometric key
val loginOutput = challenge.verify(authenticationKey).getSuccessOrNull()!!
Android - Authenticate User (after v4.0.0)
val bioUserAuthentication = ...
val promptConfig = BiometricPromptConfig(
it.wysiwys.name,
subtitle ="Authentication is required to continue",
negativeButtonText = "Close",
isConfirmationRequired = true
)
bioUserAuthentication.authenticate(activity, promptConfig) { result: Either<ErrorOutput, BiometricLoginChallenge> ->
val challenge = result.getSuccessOrNull()!!
...//continue with scenario
}
Android - UnlockKey (before v4.0.0)
val keyWrapper = ...
val promptInfo = BiometricPrompt.PromptInfo.Builder()...build();
//activity or fragment
keyWrapper.unlockKey(this@MainActivity, promptInfo, object : AuthenticationResult {
override fun authenticationSuccessful(authenticationKey: AuthenticationKey) {
...//continue with scenario
}
override fun authenticationFailed(error: ErrorOutput) {}
})
iOS
let loginInput = ...
let authGtwCmCommunicator = CommunicatorFactory.createForAuthGtwCm() //with client cert
let federatedLoginCommunicator = CommunicatorFactory.createForFederatedLogin() else { return }
let bioLogin = BiometricLogin(
authGatewayCaseMobileCommunicator: authGtwCmCommunicator,
federatedLoginCommunicator: federatedLoginCommunicator)
let challenge = bioLogin.getChallenge(loginInput: loginInput)
let bioLoginChallenge = try challenge.get()
let verify = bioLoginChallenge.verify(
biometricPrompt: "Přihlašte se biometrií.")
let loginOutput = try verify.get()
TBD
Activation is enrolling process of MEPi SDK. During activation data required for some MEPi SDK features are generated. It has 4 stages:
MEPi.StatusFactory
)
applicationActivated
property of Status
instance. If it is false
activation is required. Otherwise, activation is not needed and login is sufficient.MEPi.Activation
)
FSi or CMiTP
)
MEPi.Activation
)
MEPi.StatusFactory
MEPi.Status
MEPi.Activation
val authGtwCmNetworkCall = ...
val activation = Activation(authGtwCmNetworkCall)
val instanceIdResult = activation.getInstanceId()
val instanceId = instanceIdResult.getSuccessOrNull()!!
val keyWrapper = activation.getBioUnlockableKey().getSuccessOrNull()!!
val authenticationKey = ... // unlocked biometric key
val clientId = ... // client id of application
val accessToken = ... // token from login
val result = activation.issueCertificates(
accessToken,
clientId,
instanceId,
key)
guard let communicator = CommunicatorFactory.createForAuthGtwCm()
let activation = Activation.init(authGatewayCaseMobileCommunicator: communicator)
let instanceIdResult = activation.getInstanceId()
let instanceId = instanceIdResult.get()
let clientId = ... // client id of application
let accessToken = ... // token from login
let result = activation.issueCertificates(accessToken: accessToken,
clientId: clientId,
instanceId: instanceId,
biometricPrompt: "Gimme your biometrics!")
Reactivation is process of renewing an almost-expired instance in MEPi SDK. This scenario requires valid app certificate and access token assigned with proper scopes. During activation data required for some MEPi SDK features are renewed:
MEPi.Reactivation
MEPi.Status
val authGtwCmNetworkCall = NetworkCall("${serverUrl}/mep/fs/svc/authgtw-cm")
val reactivation = Reactivation(authGtwCmNetworkCall)
val keyWrapper = reactivation.getBioUnlockableKey().getSuccessOrNull()!!
val authenticationKey = ... // unlocked biometric key, if biometrics were activated before
val clientId = ... // client id of application
val accessToken = ... // token from login, with proper scope
val result = reactivation.reactivate(accessToken, clientId, authenticationKey)
val newInstanceId = result.getSuccessOrNull()!!
guard let communicator = CommunicatorFactory.createForAuthGtwCm()
let reactivation = Reactivation.init(authGatewayCaseMobileCommunicator: communicator)
let clientId = ... // client id of application
let accessToken = ... // token from login, with proper scope
let reactivateWithBiometry = false // or true if biometrics were activated before
let result = reactivation.reactivate(accessToken: accessToken, clientId: clientId, useBiometry: reactivateWithBiometry)
let newInstanceId = try result.get()
If it was requested at the start of a login, ID token is parsed to LoginOutput
returned from login scenarions. Both standard and non-standard data can be retrieved from returned IDToken
instance.
let loginOutput = ... // output of login scenario
let idToken = loginOutput.idToken
let subject = idToken?.subject // standard value
let sessionState: String? = idToken?.get(parameter: Keys.session_state) // non-standard value
enum Keys: String, CodingKey { // enum with non-standard values
case session_state
...
}
Initial client certificate is used to authenticate mobile application during network calls in first login scenario. The certificate is valid only for a few minutes. Mobile Application can request issuing of initial certificate from server before login scenario or during scenarion if previous certificate expires.
MEPi.InitialClientCertificates
Android
val caseMsCommunicator = NetworkCall("${serverUrl}/casems/attestation/")
val safetyNetAPIKey = ...
val clientId = ... // client id of application
val appAttestationId = ... // application id value
val context = ...
val initClientCertificate = InitialClientCertificate(context, caseMsCommunicator, safetyNetAPIKey)
val certificate: InitialClientCertificateResult = clientCertificate.requestInitialClientCertificate(clientId, appAttestationId).getSuccessOrNull()
iOS
guard let caseMsCommunicator = CommunicatorFactory.createForCaseMs() else { return }
let clientId = ... // client id of application
let attestationId = ... // bundle id value
let initClientCertificate = InitialClientCertificate(caseMsCommunicator: caseMsCommunicator)
let certificateResult = initialClientCertificate.requestInitialClientCertificate(clientId: clientId
appAttestationId: attestationId)
let clientCertificate: InitialClientCertificateResult = try clientCertificateResult.get()
Initial client certificate issued by MEP (see previous scenario) are valid only for short period (few minutes - based on backend configuration). When any login scenario takes too long to complete, initial certificate might expire. To resolve this, application can request another initial certificate, set it to class representing interrupted login step and continue with the login scenario. Classes in MEPi SDK, that are calling endpoints protected by initial client certificates provide API to update client certificate if needed.
MEPiCommons.SslContextChangeable
(Android)MEPiCommons.ContainsSessionDelegateChangeable
(iOS)
Android
val updatedSslContext = ... // new instance of sslContext
val changeable: X = ... // where X is one of classes below
changeable.setSslContext(updatedSslContext)
// possible types for X:
// FSiLogin
// Scenario
// LoginFinish
// Sms
// UserNameAndPassword
// Activation
iOS
let updatedDelegate = ... // new instance of session delegate
let changeable: X = ... // where X is one of classes below
changeable.changeSessionDelegate(to: updatedDelegate)
// possible types for X:
// LoginOutput
// FSiLogin
// LoginFinish
// SMS
// UserNameAndPassword
// Activation
Preconditions for this scenario:
Authorization of transaction has 3 stages:
FSi.FSiTransaction
let accessToken = ... // token from login
let mepId = ... // mep id from transaction registration
guard let communicator = CommunicatorFactory.createForAuthGtwFta() else { return }
do {
let trx = FSiTransaction(
authGatewayFederatedTransactionAuthorizationCommunicator: communicator,
accessToken: accessToken
)
let lang = "cs" // BE must support trx templates for selected language
let trxResult = trx.startTransaction(mepId: mepId, language: lang)
FSi.TransactionScenario
let scenario = try trxResult.get() // or switch on Result.success/Result.failure
let requiredScenario = "..."
guard scenario.scenariosId.contains(requiredScenario) else {
failed(message: "Does not support '...' scenario.")
return
}
FSi.TransactionSMS
let requiredScenario = "s_mobile_authz_sms"
let smsResult = scenario.selectSMS(scenario: requiredScenario)
let sms = try confirmationResult.get()
let smsCode = ... // show UI with SMS input field & wait user input
let result = sms.submit(sms: smsCode)
try result.get()
FSi.TransactionConfirmation
let requiredScenario = "s_mobile_authz_none"
let confirmationResult = scenario.selectConfirmation(scenario: requiredScenario)
let confirmation = try confirmationResult.get()
let result = confirmation.confirm()
try result.get()
TBD
After multiple failed attempts to autheticate user with biometric, OS will block its consecutive usage. This can be reverted by entering device credential (pattern/PIN/password on Android, passcode on iOS). An application can offer dialog that will promp user to enter device credential. After successful verification, user can use biometrics again.
MEPi.BiometricUnlocker
MEPi.BiometricPromptConfig
val promptConfig = BiometricPromptConfig(
"Biometry unlock",
subtitle ="Authentication is required to continue",
negativeButtonText = "Close",
isConfirmationRequired = true
)
BiometricUnlocker().unlock(activity, promptConfig) { result: Boolean ->
..// show scenario result to user
}
BiometricUnlocker().unlock(localizedReason: "Biometry unlock") { [weak self] success in
..// show scenario result to user
}
If any operation should fail in MEPi SDK, details about the error are returned to the integrating application. An application can use those data to inform the user about failure and/or write them to logs.
Every error returned from MEPi SDK is encapsulated in same simple data structure containing related info.
MEPiCommons.ErrorOutput
MEPiCommons.MEPiError
link |
Stars: 0 |
Last commit: 1 week ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics