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: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(WKNavigationActionPolicy.allow)
return
}
let navPolicy = self.handleNavigationActionPolicyFor(url: url.absoluteString)
decisionHandler(navPolicy)
}
func handleNavigationActionPolicyFor(url: String) -> WKNavigationActionPolicy {
/*
README
create one bigger string for logs to have all information of the
process in one log line.
OTHERWISE, if using multiple log lines, there is the problem that
multiple processes (also master controller processes) are running
in the background and they log a lot so that our logs here would be
spread out in the log file and it would be a little bit difficult to
accumulate them. using only one log line, we can save the effort.
*/
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. "
/*
to continue with the IAM activation or login, the authorization code
and the access token is needed.
If they are found, then the navigation action needs to be canceled
example requestURL:
https://someRedirect.url
#state=53d...196
&session_state=687...45b
&code=6c0...5db
&access_token=eyJ...JZQ
*/
var policyResult: WKNavigationActionPolicy = WKNavigationActionPolicy.allow
let authorizationCodeIndicator = "&code="
let accessTokenIndicator = "&access_token="
if url.contains(authorizationCodeIndicator) == true
&& url.contains(accessTokenIndicator){
// get code and token
let result = self.getAuthorizationCodeAndAccessToken(from: url)
logString += "found authorizationCode '\(result.authorizationCode)' and accessToken character count
` `'\(result.accessToken.count)'. "
// callback delegate
logString += "now going to callback delegate (\(String(describing: self.delegate))"
self.delegate?.received(authorizationCode: result.authorizationCode,
accessToken: result.accessToken)
policyResult = WKNavigationActionPolicy.cancel
}
else {
logString += "no authorization code and no access token found. "
policyResult = WKNavigationActionPolicy.allow
}
Logger.sharedLogger.logInfo(content: logString,
context: "\(type(of: self)).\(#function)")
return policyResult
}
func getAuthorizationCodeAndAccessToken(from url: String) -> (authorizationCode: String, accessToken: String) {
/*
example requestURL:
https://someRedirect.url
#state=53d...196
&session_state=687...45b
&code=6c0...5db
&access_token=eyJ...JZQ
*/
let authorizationCodeIndicator = "&code="
let authorizationCode = self.cutOutUrlParameterWith(indicator: authorizationCodeIndicator,
fromString: url)
let accessTokenIndicator = "&access_token="
let accessToken = self.cutOutUrlParameterWith(indicator: accessTokenIndicator,
fromString: url)
return (authorizationCode, accessToken)
}
func cutOutUrlParameterWith(indicator: String, fromString: String) -> String {
/*
example string:
https://someRedirect.url
#state=53d...196
&session_state=687...45b
&code=6c0...5db
&access_token=eyJ...JZQ
*/
var resultString: String = ""
// value of parameter starts when the indicator ends
let parameterStartIndex = fromString.endIndex(of: indicator)
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]
/*
substring is now sth like for '&code': 6c0...5db
&access_token=eyJ...JZQ
*/
// now cut out the string until the next '&'. this will then be the value of the parameter
let parameterEndIndex = substring.index(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 activateWithToken(user: String, tenant: String, token: String, autoLogin: Bool) {
let identifier = KsUserIdentifier(tenantId: tenant, userId: user)
let event = KSMActivateWithTokenEvent(userIdentifier: identifier, token: token, enableAutoLogin: autoLogin)
MasterControllerAdapter.sharedInstance.sendEvent2MasterController(event: event) { event in
if let activationResult = event as? KSMActivationResultEvent {
print(activationResult)
}
}
}
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)
launchIO {
mcEventHandler?.postEvent(activateWithTokenEvent)?.then {
// handle result
}
}
}