Activation
In environments with IDP, the app is connecting to the IDP server in a web view to do the user registration with project specific user authentication and getting some IDP token. With this token the MC can activate the user account for this app installation with the Security Server. Activation code/pin as in Digitanium is not needed. In addition userId and tenantId is needed as in Digitanium solutions. See EventList for IDP activation.
Proceed with such an activation if you receive a StartResultEvent with ACTIVATION_REQUIRED as sdkState as a response to the StartEvent.
For this configuration, add this JSON section to your app_config.json file.
"identityAccessManagementInformation": {
"loginActivationWebpageUrl" : "https://someUrl", // the URL to the webpage to load on app start
"clientId" : "mpower-app", // the value for client_id that is used in many calls to the IAM
"scope" : "openId", // the scope value needed by OpenID process
"getAuthorizationCodeUrl": "https://{ecoId}.api.midentity.one/digitanium/v1/auth",
// the URL to get the authorization code. "{ecoId}" needs to be replaced with the actual ecoId/tenantId
"getTokenUrl" : "https://{ecoId}.api.midentity.dev/digitanium/v1/login",
// the URL to get the token and to refresh the token. "{ecoId}" needs to be replaced with the actual ecoId/tenantId
"serverCertificateTrustStore": ["identitiyAccessManagementCertificates.pem"],
"clientAuthentication": null
},
(...)
"userActivationLoginType" : "tokenBased",
// this value could be set as optional and if not existing the app should work pin based as default setting
(...)
After successful login with your userId and password on the IAM web login page, you will receive a URL from the IAM webPage that contains your access token and authorization code. The TWVClient will call the sendOpenIdRedirectUriCode(url: String) callback method. The extraction of the authorization code and access token needs to be part of the implementation of the latter mentioned callback method.
https://kobil/OpenIdRedirectUri#session_state=d357a207-b46e-455a-8947-16bb8953ec20&code=054e42f9-38c4-4e1b-aaeb-aa2b7e4927ea.<br/>d357a207-b46e-455a-8947-16bb8953ec20.b4d1d026-8701-42b3-9528-f6803ac698d4&access_token=<br/>eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3QkhrSEFlblhtR<br/>TcwaDBZd2RueEE5Y1pZVFRkczZYb1NkSW4zQ0FMYmRBIn0.eyJqdGkiOiJmN2YwY2U2My0yNTc1LTRjNWEtOTc3ZC1iNTUw<br/>ZTc2YTE2MzEiLCJleHAiOjE2MDc2ODExOTcsIm5iZiI6MCwiaWF0IjoxNjA3NjgwODk3LCJpc3MiOiJodHRwczovL21wb3dlcjNnY<br/>W1tYS5hcGkudHNlMDMudHNlLmRldi5rb2JpbC5jb20vYXV0aC9yZWFsbXMvbXBvd2VyM2dhbW1hIiwiYXVkIjpbIlNTTVMiLCJhY<br/>2NvdW50Il0sInN1YiI6ImNkYWU3ODMwLWE4YTItNDNiNi1hNzYyLTkzZTc4YjgyNTQwNSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im1wb3<br/>dlci1hcHAiLCJub25jZSI6IjBkYmU0YmIzNzE0ZDQwNGZhY2JmNGI3YzlkNjU2OTUwIiwiYXV0aF90aW1lIjoxNjA3NjgwODk3LCJzZX<br/>NzaW9uX3N0YXRlIjoiZDM1N2EyMDctYjQ2ZS00NTVhLTg5NDctMTZiYjg5NTNlYzIwIiwiYWNyIjoiMSIsImFtciI6ImtfcHdkIiwiYWxs<br/>b3dlZC1vcmlnaW5zIjpbImh0dHBzOi8va29iaWwiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiZ<br/>GlnaXRhbml1bV91c2VyIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm<br/>1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZC<br/>Bwcm9maWxlIGFzdC1hY3RpdmF0aW9uIGVtYWlsIGFjcjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJGaXJzdG5hb<br/>WUgTGFzdG5hbWUiLCJ0ZW5hbnRJZCI6Im1wb3dlcjNnYW1tYSIsInByZWZlcnJlZF91c2VybmFtZSI6Imlvc3Rlc3R1c2VyMSIsI<br/>mdpdmVuX25hbWUiOiJGaXJzdG5hbWUiLCJmYW1pbHlfbmFtZSI6Ikxhc3RuYW1lIiwiZW1haWwiOiJpb3N0ZXN0dXNlcjFAa29ia<br/>WwuY29tIiwic3Ntc19yZXNvdXJjZV9hY2Nlc3MiOnsicGVybWlzc2lvbnMiOnsiYXN0LWFjdGl2YXRpb24iOnRydWUsImFzdC1sb2d<br/>pbiI6dHJ1ZX19fQ.BaYfNZet2TQ1Y7QIjWZBepf4fYU8WHCee9oPR-RYz6fYq_zCaVlANmXTkrKjfgIJV49_T8XdnG75JVnD1xTuz5HakkOp9TUNTKP9vMx26J5ATeWc3nHldlL6z-8xrY7LvqD32xn694LHv7R3t825b5qdgst18myEgtCJ_P3zcHmIG8ENc7B8e34f8_8arGwrOz8VxtzuiL6WdUElOgZvFamflLuYwzm03TiWjDsOZxTVqcMbKh-gNl68QKxvRe23xAXaha8gQkGXA2ttAdFiMGTGIh3XBh3atXfuuyb3Oh0aaDBX7HTSUWR-osOpR7JLiyHt31EAjlAq_cclo5Z-9w](https://kobil/OpenIdRedirectUri#session_state=d357a207-b46e-455a-8947-16bb8953ec20&code=054e42f9-38c4-4e1b-aaeb-aa2b7e4927ea.d357a207-b46e-455a-8947-16bb8953ec20.b4d1d026-8701-42b3-9528-f6803ac698d4&access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3QkhrSEFlblhtRTcwaDBZd2RueEE5Y1pZVFRkczZYb1NkSW4zQ0FMYmRBIn0.eyJqdGkiOiJmN2YwY2U2My0yNTc1LTRjNWEtOTc3ZC1iNTUwZTc2YTE2MzEiLCJleHAiOjE2MDc2ODExOTcsIm5iZiI6MCwiaWF0IjoxNjA3NjgwODk3LCJpc3MiOiJodHRwczovL21wb3dlcjNnYW1tYS5hcGkudHNlMDMudHNlLmRldi5rb2JpbC5jb20vYXV0aC9yZWFsbXMvbXBvd2VyM2dhbW1hIiwiYXVkIjpbIlNTTVMiLCJhY2NvdW50Il0sInN1YiI6ImNkYWU3ODMwLWE4YTItNDNiNi1hNzYyLTkzZTc4YjgyNTQwNSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im1wb3dlci1hcHAiLCJub25jZSI6IjBkYmU0YmIzNzE0ZDQwNGZhY2JmNGI3YzlkNjU2OTUwIiwiYXV0aF90aW1lIjoxNjA3NjgwODk3LCJzZXNzaW9uX3N0YXRlIjoiZDM1N2EyMDctYjQ2ZS00NTVhLTg5NDctMTZiYjg5NTNlYzIwIiwiYWNyIjoiMSIsImFtciI6ImtfcHdkIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHBzOi8va29iaWwiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiZGlnaXRhbml1bV91c2VyIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGFzdC1hY3RpdmF0aW9uIGVtYWlsIGFjcjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJGaXJzdG5hbWUgTGFzdG5hbWUiLCJ0ZW5hbnRJZCI6Im1wb3dlcjNnYW1tYSIsInByZWZlcnJlZF91c2VybmFtZSI6Imlvc3Rlc3R1c2VyMSIsImdpdmVuX25hbWUiOiJGaXJzdG5hbWUiLCJmYW1pbHlfbmFtZSI6Ikxhc3RuYW1lIiwiZW1haWwiOiJpb3N0ZXN0dXNlcjFAa29iaWwuY29tIiwic3Ntc19yZXNvdXJjZV9hY2Nlc3MiOnsicGVybWlzc2lvbnMiOnsiYXN0LWFjdGl2YXRpb24iOnRydWUsImFzdC1sb2dpbiI6dHJ1ZX19fQ.BaYfNZet2TQ1Y7QIjWZBepf4fYU8WHCee9oPR-RYz6fYq_zCaVlANmXTkrKjfgIJV49_T8XdnG75JVnD1xTuz5HakkOp9TUNTKP9vMx26J5ATeWc3nHldlL6z-8xrY7LvqD32xn694LHv7R3t825b5qdgst18myEgtCJ_P3zcHmIG8ENc7B8e34f8_8arGwrOz8VxtzuiL6WdUElOgZvFamflLuYwzm03TiWjDsOZxTVqcMbKh-gNl68QKxvRe23xAXaha8gQkGXA2ttAdFiMGTGIh3XBh3atXfuuyb3Oh0aaDBX7HTSUWR-osOpR7JLiyHt31EAjlAq_cclo5Z-9w)"
Activation event flow-diagram
The following diagram depicts the event flow of the activation process
NOTE: using ActivateWithTokenEvent with autoLogin=true will perform an automatic login upon completing the activation, BUT it will NOT save any tokens in the key storage of the MC. Meaning that the next time the user returns to the app, no tokens will be available, and therefore an IDP login is required. Tokens will only be stored when using LoginWithTokenEvent.
iOS/Swift
Refer to this code to extract your authorization code and your access token from the above URL. The tenantId and userId will be extracted from the access token:
func webView(_ webView: KsTrustedWebView, shouldHandleExternalUrl navigationAction: WKNavigationAction?, decisionHandler: ((Bool) -> Void)? = nil) {
guard let url = navigationAction?.request.url else {
decisionHandler?(true)
return
}
let navPolicy = self.handleNavigationActionPolicyFor(url: url.absoluteString)
decisionHandler?(navPolicy)
}
func handleNavigationActionPolicyFor(url: String, completion: @escaping (ActionResult) -> Void = { _ in }) -> Bool {
var logString = "webview tries to load this URL now: \(url). "
logString += "now going to check if the requested URL contains authorization code and access token. "
/*
example requestURL:
https://someRedirect.url
#state=53d...196
&session_state=687...45b
&code=6c0...5db
&access_token=eyJ...JZQ
*/
var policyResult = true
let authorizationCodeIndicator = "&code="
let urlBeginWith = "https://kobil/OpenIdRedirectUri"
if url.contains(authorizationCodeIndicator) == true
&& url.starts(with: urlBeginWith) {
// get authorization code
let code = self.getAuthorizationCode(from: url)
let token = self.getAccessToken(from: url)
setCode(code: code, token: token, completion: completion)
logString += "found authorizationCode '\(code)"
policyResult = false
} else {
logString += "no authorization code and no access token found. "
policyResult = true
}
return policyResult
}
func getAuthorizationCode(from url: String) -> (String) {
let authorizationCodeIndicator = "&code="
let authorizationCode = self.cutOutUrlParameterWith(indicator: authorizationCodeIndicator,
fromString: url)
return (authorizationCode)
}
func cutOutUrlParameterWith(indicator: String, fromString: String) -> String {
var resultString: String = ""
// value of parameter starts when the indicator ends
let parameterStartIndex = fromString.range(of: indicator)?.upperBound
if parameterStartIndex == nil {
return resultString
}
// now cut out a string starting with the value until end of string
let endIndex = fromString.index(fromString.endIndex, offsetBy: 0)
let range = parameterStartIndex!..<endIndex
let substring = fromString[range]
// now cut out the string until the next '&'. this will then be the value of the parameter
let parameterEndIndex = substring.firstIndex(of: "&")
if parameterEndIndex != nil {
resultString = String( substring[substring.startIndex..<parameterEndIndex!] )
} else {
// if '&' not found, then the parameter is the substring
resultString = String( substring )
}
return resultString
}
When you have all the required values for activation you can trigger ActivateWithTokenEvent and as a response you will receive an ActivationResultEvent, or a LoginResultEvent if you use autoLogin=true.
func activateOrLogin(credentials: UserCredentials,
token: String,
code: String,
auth: KSMAuthenticationMode,
completion: @escaping (ActionResult) -> Void) {
self.completion = completion
var userList = [User]()
if let currentState = StateMachine.sharedInstance.getCurrentState() as? UserActivated {
userList = currentState.userList
}
let user = KsUserIdentifier(tenantId: credentials.tenantId, userId: credentials.userId)
if userList.contains(where: { $0.userName.contains(credentials.userId) }) {
performLogin(user: user, auth: auth, token: token, code: code)
} else if userList.isEmpty {
performActivation(user: user, token: token)
} else {
performAddUser(user: user, token: token)
}
}
private func performLogin(user: KsUserIdentifier, auth: KSMAuthenticationMode, token: String, code: String) {
let loginEvent = KSMLoginWithTokenEvent(parameters: user,
authenticationMode: auth,
iamAccessToken: token,
authorizationCode: code)
let timer = CallTimer(event: loginEvent)
MasterControllerAdapter.sharedInstance.sendEvent2MasterController(event: loginEvent) { [weak self] resultEvent in
self?.handleLoginEvent(resultEvent: resultEvent, timer: timer)
}
}
private func performActivation(user: KsUserIdentifier, token: String) {
let activateEvent = KSMActivateWithTokenEvent(userIdentifier: user, token: token, enableAutoLogin: true)
let timer = CallTimer(event: activateEvent)
MasterControllerAdapter.sharedInstance.sendEvent2MasterController(event: activateEvent) { [weak self] resultEvent in
self?.handleActivationEvent(resultEvent: resultEvent, timer: timer)
}
}
Android/Kotlin
class KsWebView : WebView {
private var activity: BaseActivity? = null
private var isEnableHistoryBack: Boolean = false
private var twvClient: TWVClient? = null
...
fun enablePinningProxyIfNeeded(activity: BaseActivity) {
twvClient = TWVClient(object : TWVCallBack {
...
override fun sendOpenIdRedirectUriCode(url: String) {
// here you can implement your logic to extract the needed tokens from the URI
}
...
})
...
}
}
class IamHelper {
companion object {
private val AUTH_CODE_MARKER = "&code="
private val ACCESS_TOKEN_MARKER = "&access_token="
private val REDIRECT_URI = "https://kobil/OpenIdRedirectUri"
private val RESPONSE_TYPE = "code%20token"
private val RESPONSE_MODE = "fragment"
/**
* This function parses the url after a successful Login to the IAM webpage
* to get the authorization code and access token.
* The url looks as follows: "https://kobil/OpenIdRedirectUri&code=<authorization_code>&access_token=<access_token>"
* For maverick, the url contains only the authorization code: "https://kobil/OpenIdRedirectUri&code=<authorization_code>"
* @return an array containing the authorization code as its first element and the access token as the second element.
* For maverick, the second element will be an empty string
*/
fun parseUrl(url : String): Array<String> {
logDebug("parsing url: $url")
var a = Array<String>(2) { i -> "" }
val authStart = url.indexOf(AUTH_CODE_MARKER, 0)
var sub1 = url.substring(authStart + AUTH_CODE_MARKER.length)
logDebug("parsing url sub1: $sub1")
val authEnd = sub1.indexOf("&", 0) - 1
// in the case of maverick we only get the authorization code so we pare it and return the result
if (authEnd < 0) {
a[0] = sub1
a[1] = ""
return a
}
val authCode = sub1.substring(0..authEnd)
a[0] = authCode
val accessStart = sub1.indexOf(ACCESS_TOKEN_MARKER, 0)
var authToken = sub1.substring(accessStart + ACCESS_TOKEN_MARKER.length)
a[1] = authToken
return a
}
fun getUserIdAndTenantIdFromAuthToken(token: String): Pair<String, String> {
if (token.isNullOrEmpty()) {
return Pair("", KsAppConfigManager.getEntityInstance().tenantId!!)
}
var split = token.split(".")
logDebug("splits = ${split.size}\nsplit[0]=${split[0]}", "getUserIdAndTenantIdFromAuthToken")
for (elem in split) {
var decodedString = decodeBase64String(elem)
val jsonObject = JSONObject(decodedString)
// userId (in the token userId is saved with the key sub)
if (!jsonObject.has("sub")) continue
val userId = jsonObject["sub"] as String
// tenantId
if (!jsonObject.has("sub")) continue
val tenantId = jsonObject["tenantId"] as String
return Pair(userId, tenantId)
}
return Pair("", "")
}
private fun decodeBase64String(input: String): String {
var stringToUse = input.replace("\n", "")
stringToUse = stringToUse.replace("\r", "")
stringToUse = stringToUse.replace("_", "/")
stringToUse = stringToUse.replace("-", "+")
// padding
while (stringToUse.length % 4 != 0) {
stringToUse += "="
}
logDebug("stringToDecode=$stringToUse", "decodeBase64String", "KsWebView")
val decodedBytes = Base64.decode(stringToUse, Base64.DEFAULT)
return String(decodedBytes)
}
fun isTokenExpired(token: String) : Boolean {
var split = token.split(".")
for (elem in split) {
var decodedString = decodeBase64String(elem)
val jsonObject = JSONObject(decodedString)
if (!jsonObject.has("exp")) continue
val expiration = jsonObject["exp"] as Int // expiration is in sec
val now = System.currentTimeMillis() / 1000 // time in sec
val check = expiration <= now
logDebug("expirationDate=$expiration now=$now after=$check", "isTokenExpired", "IamHelper")
return check
}
return true
}
fun getIamWebPageUrl(baseUrl: String, clientId: String, scope: String, context: Context) : String {
var result = baseUrl
var uuid = Settings.Secure.getString(context?.contentResolver, Settings.Secure.ANDROID_ID)!!
result += "?"
result += "client_id=$clientId"
result += "&"
result += "redirect_uri=$REDIRECT_URI"
result += "&"
result += "scope=$scope"
result += "&"
result += "response_type=$RESPONSE_TYPE"
result += "&"
result += "response_mode=$RESPONSE_MODE"
result += "&"
result += "nonce=${uuid.toLowerCase().replace("-", "")}"
return result
}
}
}
When you have all the required values for activation you can trigger ActivateWithTokenEvent and as a response you will receive an ActivationResultEvent, or a LoginResultEvent if you use autoLogin=true.
fun triggerActivateWithTokenEvent(
userIdentifier: UserIdentifier,
token: String,
autoLogin: Boolean
) {
val activateWithTokenEvent = ActivateWithTokenEvent(userIdentifier, token, autoLogin)
synchronousEventHandler.postEvent(activateWithTokenEvent)?.then {
// handle result
}
}