Java tutorial
/* * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ package org.wso2.carbon.identity.authenticator.mobileconnect; import org.apache.axiom.util.base64.Base64Utils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.oltu.oauth2.client.request.OAuthClientRequest; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.apache.oltu.oauth2.common.utils.JSONUtils; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import org.wso2.carbon.identity.application.authentication.framework.FederatedApplicationAuthenticator; import org.wso2.carbon.identity.application.authentication.framework.LocalApplicationAuthenticator; import org.wso2.carbon.identity.application.authentication.framework.config.ConfigurationFacade; import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; import org.wso2.carbon.identity.application.authentication.framework.exception.ApplicationAuthenticatorException; import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.authenticator.oidc.OpenIDConnectAuthenticator; import org.wso2.carbon.identity.application.common.model.ClaimMapping; import org.wso2.carbon.identity.application.common.model.Property; import org.wso2.carbon.identity.application.common.util.IdentityApplicationConstants; import org.wso2.carbon.identity.base.IdentityConstants; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.user.core.UserRealm; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.service.RealmService; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Authenticator of Mobile Connect. * The MobileConnectAuthenticator class carries out the Discovery API Process and the Mobile Connect API process */ public class MobileConnectAuthenticator extends OpenIDConnectAuthenticator implements FederatedApplicationAuthenticator { private static final long serialVersionUID = -8755624597283931608L; private static Log log = LogFactory.getLog(MobileConnectAuthenticator.class); /** * Check whether the authentication or logout request can be handled by the * authenticator. */ public boolean canHandle(HttpServletRequest request) { //this condition is to control the status of the UI flow if ((MobileConnectAuthenticatorConstants.MOBILE_CONNECT_UI_PROCESS_COMPLETE).equals( request.getSession().getAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_UI_STATUS))) { request.getSession().setAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_UI_STATUS, ""); return true; } else if ((MobileConnectAuthenticatorConstants.MOBILE_CONNECT_OPERATOR_SELECTION_DONE .equals(request.getSession().getAttribute( MobileConnectAuthenticatorConstants.MOBILE_CONNECT_OPERATOR_SELECTION_STATUS)))) { //to check whether the operator selection process is completed request.getSession() .setAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_OPERATOR_SELECTION_STATUS, ""); return false; } else if (request.getParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_MCC_MNC) != null) { //to check if the request is carrying a mcc_mnc parameter request.getSession().setAttribute( MobileConnectAuthenticatorConstants.MOBILE_CONNECT_OPERATOR_SELECTION_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_OPERATOR_SELECTION_DONE); return true; } else { //return false if OIDC authorization process is incomplete return request.getParameter(MobileConnectAuthenticatorConstants.OIDC_CODE) != null && request.getParameter(MobileConnectAuthenticatorConstants.OIDC_STATE) != null && MobileConnectAuthenticatorConstants.MOBILE_CONNECT_LOGIN_TYPE .equals(this.getLoginType(request)) || request.getParameter(MobileConnectAuthenticatorConstants.OIDC_STATE) != null && request.getParameter(MobileConnectAuthenticatorConstants.OIDC_ERROR) != null; } } /** * Get the login type of the request and identify the authenticator. */ private String getLoginType(HttpServletRequest request) { String state = request.getParameter(MobileConnectAuthenticatorConstants.OIDC_STATE); return state != null ? state.split(",")[1] : null; } /** * Initiate the Authentication request when the AuthenticatorFlowStatus is INCOMPLETE. */ @Override protected void initiateAuthenticationRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { //retrieve the properties configured Map<String, String> authenticatorProperties = context.getAuthenticatorProperties(); //retrieve the authentication type for mobile connect String authenticationType = authenticatorProperties .get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHENTICATION_TYPE); //this will store the mobile number which is being used in the flow String msisdn = null; //check whether the process is multi-step authentication if (context.getSequenceConfig().getStepMap().size() > 1) { //get subject details from the Authentication provided before AuthenticatedUser authenticatedUser = getAuthenticatedUser(context); if (authenticatedUser != null) { //get UserRealm UserRealm userRealm = getUserRealm(authenticatedUser.getTenantDomain()); //Get Tenant Aware Username if (userRealm != null) { try { //retrieve username from the user stores msisdn = userRealm.getUserStoreManager().getUserClaimValue( authenticatedUser.getUserStoreDomain() + "/" + authenticatedUser.getUserName(), MobileConnectAuthenticatorConstants.MOBILE_CONNECT_MOBILE_CLAIM, null); if (StringUtils.isNotEmpty(msisdn)) { //set the mobile number in the context context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_MOBILE_NUMBER, msisdn); //change the flow of authentication directly Discovery Endpoint context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ENDPOINT); } } catch (UserStoreException e) { throw new AuthenticationFailedException( "Cannot find the user claim for mobile " + e.getMessage(), e); } } } } //If the enforce type is on-net & that the msisdn is null if ((MobileConnectAuthenticatorConstants.MOBILE_CONNECT_ON_NET).equals(authenticationType) && StringUtils.isEmpty(msisdn)) { //let the context know that the system is enforcing on-net context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_ON_NET_STATUS, "true"); //get encoded authorization header String authorizationHeader = getAuthorizationHeader(authenticatorProperties); //check whether the flow status has already discovered the mcc and mnc if ((MobileConnectAuthenticatorConstants.MOBILE_CONNECT_MCC_MNC) .equals(context.getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS))) { HttpURLConnection connection; try { //get connection from discovery endpoint with mnc, mnc connection = callDiscoveryWithMccMnc(request, authenticatorProperties, authorizationHeader); } catch (IOException e) { throw new AuthenticationFailedException("Connection to Discovery API failed", e); } try { //Read contents retrieved from the Discovery Endpoint discoveryEndpointRead(context, response, connection); } catch (JSONException e) { throw new AuthenticationFailedException("Parsing the JSON object failed", e); } //if the current flow status is in Authorization Phase if (MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ENDPOINT.equals( context.getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS))) { //call this method to decode the response sent from the Discovery Endpoint and connect with the // authorization endpoint invokeAuthorizationEndpoint(context, response); } } else { //carryout the process of connecting the Discovery Endpoint for on-net Operator Selection operatorSelectionProcess(authorizationHeader, context, response, request); //set property in context to ensure that operator selection is being carried out in the flow context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_MCC_MNC); } //execute this section if the Authentication Type is Off-Net (default is off-net) } else { if (context.getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS) == null && StringUtils.isEmpty(msisdn)) { redirectToMobileNumberUI(request, response, context); } else { if (StringUtils.isEmpty(msisdn)) { //check whether the msisdn is sent by the service provider msisdn = request.getParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_MSISDN); } //retrieve the properties configured authenticatorProperties = context.getAuthenticatorProperties(); //this is null, if no such properties are defined in the IS as an IDPqq if (authenticatorProperties != null) { //MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS is the property set by the IDP, // to keep track of the authentication process if (MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ENDPOINT.equals( context.getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS))) { //get encoded authorization header String authorizationHeader = getAuthorizationHeader(authenticatorProperties); try { //call this method to retrieve a HttpURLConnection object HttpURLConnection connection = discoveryProcess(authorizationHeader, msisdn, authenticatorProperties); //carryout the process of connecting the Discovery Endpoint discoveryEndpointRead(context, response, connection); } catch (JSONException | IOException e) { throw new AuthenticationFailedException("connection to Discovery Endpoint failed", e); } } if (MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ENDPOINT.equals( context.getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS))) { //call this method to decode the response sent from the Discovery Endpoint and connect with the // authorization endpoint invokeAuthorizationEndpoint(context, response); } } else { if (log.isDebugEnabled()) { log.debug("Error while retrieving properties. Authenticator Properties cannot be null"); } throw new AuthenticationFailedException(" Authenticator Properties cannot be null"); } } } } /** * Get the username of the logged in User. */ private AuthenticatedUser getAuthenticatedUser(AuthenticationContext context) { AuthenticatedUser authenticatedUser = null; for (Integer stepMap : context.getSequenceConfig().getStepMap().keySet()) { if (context.getSequenceConfig().getStepMap().get(stepMap).getAuthenticatedUser() != null && context.getSequenceConfig().getStepMap().get(stepMap).getAuthenticatedAutenticator() .getApplicationAuthenticator() instanceof LocalApplicationAuthenticator) { authenticatedUser = context.getSequenceConfig().getStepMap().get(stepMap).getAuthenticatedUser(); break; } } return authenticatedUser; } /** * Get the user realm of the logged in user. */ private UserRealm getUserRealm(String username) throws AuthenticationFailedException { UserRealm userRealm; try { int tenantId = IdentityTenantUtil.getTenantId(username); RealmService realmService = IdentityTenantUtil.getRealmService(); userRealm = (UserRealm) realmService.getTenantUserRealm(tenantId); } catch (Exception e) { throw new AuthenticationFailedException("Cannot find the user realm", e); } return userRealm; } /** * Call the discovery endpoint with mcc and mnc. */ private HttpURLConnection callDiscoveryWithMccMnc(HttpServletRequest request, Map<String, String> authenticatorProperties, String authorizationHeader) throws IOException { //retrieve mcc_mnc from the request String mccMncValue = request.getParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_MCC_MNC); //retrieve mcc and mnc from above string String mcc = mccMncValue.substring(0, 3); String mnc = mccMncValue.substring(4); //retrieve callback url String callbackURL = getCallbackUrl(authenticatorProperties); //encode query parameters mcc = URLEncoder.encode(mcc, String.valueOf(StandardCharsets.UTF_8)); mnc = URLEncoder.encode(mnc, String.valueOf(StandardCharsets.UTF_8)); callbackURL = URLEncoder.encode(callbackURL, String.valueOf(StandardCharsets.UTF_8)); //prepare queryParameters String queryParameters = MobileConnectAuthenticatorConstants.MOBILE_CONNECT_IDENTTIFIED_MCC + "=" + mcc + "&" + MobileConnectAuthenticatorConstants.MOBILE_CONNECT_IDENTTIFIED_MNC + "=" + mnc + "&" + MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_REDIRECT_URL + "=" + callbackURL; //call the discovery endpoint with the mcc and mnc String url = MobileConnectAuthenticatorConstants.DISCOVERY_API_URL + "?" + queryParameters; //create URL object URL obj = new URL(url); HttpURLConnection connection = (HttpURLConnection) obj.openConnection(); //enforce GET request method on the connection connection.setRequestMethod(MobileConnectAuthenticatorConstants.GET_METHOD); //attach headers required to contact the Discovery API connection.setRequestProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_AUTHORIZATION, authorizationHeader); connection.setRequestProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ACCEPT, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ACCEPT_VALUE); return connection; } /** * Gets the key and secret and returns the encoded authorization header. */ private String getAuthorizationHeader(Map<String, String> authenticatorProperties) throws AuthenticationFailedException { //get the mobile connect key and secret String mobileConnectKey = getMobileConnectAPIKey(authenticatorProperties); String mobileConnectSecret = getMobileConnectAPISecret(authenticatorProperties); //Base 64 encode the key and secret to attach as the header for URL connections String userPass = mobileConnectKey + ":" + mobileConnectSecret; return "Basic " + Base64Utils.encode(userPass.getBytes(StandardCharsets.UTF_8)); } /** * Prompt for Mobile Number from the IS. */ private void redirectToMobileNumberUI(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { //retrieve the url of the UI from the Configuration Files String loginEndpointUrl = getAuthenticatorConfig().getParameterMap() .get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_UI_ENDPOINT_URL); String loginPage = ""; if (StringUtils.isNotEmpty(loginEndpointUrl)) { loginPage = ConfigurationFacade.getInstance().getAuthenticationEndpointURL() .replace("authenticationendpoint/login.do", loginEndpointUrl); } //get query parameter from the context String queryParams = FrameworkUtils.getQueryStringWithFrameworkContextId(context.getQueryParams(), context.getCallerSessionKey(), context.getContextIdentifier()); //if the context is in retrying stage String retryParam = ""; if (context.isRetrying()) { retryParam = "&authFailure=true&authFailureMsg=login.fail.message"; } try { //set this status to notify the controller that the UI stage is completed request.getSession().setAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_UI_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_UI_PROCESS_COMPLETE); //redirect to the UI page response.sendRedirect(response.encodeRedirectURL(loginPage + ("?" + queryParams)) + "&authenticators=" + getName() + retryParam); } catch (IOException e) { throw new AuthenticationFailedException("Authentication failed!", e); } context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ENDPOINT); } /** * Call the Discovery Endpoint in On-Net Scenario. */ private void operatorSelectionProcess(String authorizationHeader, AuthenticationContext context, HttpServletResponse response, HttpServletRequest request) throws AuthenticationFailedException { try { //call the Discovery API to get the redirect URL HttpResponse httpResponse = operatorSelectionDiscoveryCall(authorizationHeader); //get the 302 redirect URL from the headers of the HttpResponse String url = httpResponse.getHeaders(MobileConnectAuthenticatorConstants.LOCATION_HEADER)[0].toString() .substring(10); //get session data key String sessionDataKey = getSessionDataKey(context); //set Session Data Key to the request request.getSession().setAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_SESSION_DATAKEY, sessionDataKey); //call the operator selection UI response.sendRedirect(url); } catch (IOException e) { throw new AuthenticationFailedException("Redirect to Operator Selection UI failed", e); } } /** * Return the Session Data Key when needed. */ private String getSessionDataKey(AuthenticationContext context) { //create Session Data Key for the context String queryParams = FrameworkUtils.getQueryStringWithFrameworkContextId(context.getQueryParams(), context.getCallerSessionKey(), context.getContextIdentifier()); String subStr = queryParams.substring( queryParams.indexOf(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_SESSION_DATAKEY + "=")); return subStr .substring(subStr.indexOf(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_SESSION_DATAKEY + "="), subStr.indexOf("&")) .replace((MobileConnectAuthenticatorConstants.MOBILE_CONNECT_SESSION_DATAKEY + "="), ""); } /** * Return the context identifier when needed. */ public String getContextIdentifier(HttpServletRequest request) { if (request.getSession() .getAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_CONTEXT_IDENTIFIER) == null) { request.getSession().setAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_CONTEXT_IDENTIFIER, request.getParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_SESSION_DATAKEY)); return (String) request.getSession() .getAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_SESSION_DATAKEY); } else { return (String) request.getSession() .getAttribute(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_CONTEXT_IDENTIFIER); } } /** * Process the authentication request sent by the Authorization endpoint. */ @Override protected void processAuthenticationResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { //call this method to contact the tokenEndpoint and retrieve the token tokenAuthenticationRequest(request, response, context); //get JSON object of user info endpoint from context JSONObject json = (JSONObject) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_USER_INFO_JSON_OBJECT); try { buildClaims(context, json.toString()); } catch (ApplicationAuthenticatorException e) { throw new AuthenticationFailedException("Authentication failed", e); } } /** * Handle the response received from the Discovery Endpoint and connect with the Authentication Endpoint. */ private void invokeAuthorizationEndpoint(AuthenticationContext context, HttpServletResponse response) throws AuthenticationFailedException { //retrieve the properties configured Map<String, String> authenticatorProperties = context.getAuthenticatorProperties(); //decode the json object returned from the Discovery API JSONObject jsonObject = (JSONObject) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_JSON_OBJECT); String authorizationEndpoint = ""; String tokenEndpoint = ""; String userinfoEndpoint = ""; String operatoridScope = ""; String authorizationClientId; String authorizationSecret; String subscriberId = ""; //this is maintained to check whether the authentication type is on-net or off-net String uiPromptStatus = (String) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_ON_NET_STATUS); try { JSONObject jsonResponse = jsonObject .getJSONObject(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_JSON_OBJECT); authorizationClientId = jsonResponse .getString(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_CLIENT_ID); authorizationSecret = jsonResponse .getString(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_CLIENT_SECRET); //retrieve subscriber id if off-net if (!("true").equals(uiPromptStatus)) { subscriberId = jsonObject .getString(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_SUBSCRIBER_ID); } JSONObject apis = jsonResponse .getJSONObject(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_APIS); JSONObject operatorid = apis .getJSONObject(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_OPERATOR_ID); JSONArray operatoridLink = operatorid .getJSONArray(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_LINK); for (int i = 0; i < operatoridLink.length(); i++) { String linkRef = operatoridLink.getJSONObject(i) .getString(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_LINK_REFERENCE); if (MobileConnectAuthenticatorConstants.MOBILE_CONNECT_LINKS_AUTHORIZATION.equals(linkRef)) { authorizationEndpoint = operatoridLink.getJSONObject(i).getString("href"); } else if (MobileConnectAuthenticatorConstants.MOBILE_CONNECT_LINKS_TOKEN.equals(linkRef)) { tokenEndpoint = operatoridLink.getJSONObject(i).getString("href"); } else if (MobileConnectAuthenticatorConstants.MOBILE_CONNECT_LINKS_USERINFO.equals(linkRef)) { userinfoEndpoint = operatoridLink.getJSONObject(i).getString("href"); } else if (MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_SCOPE.equals(linkRef)) { operatoridScope = operatoridLink.getJSONObject(i).getString("href"); } } } catch (JSONException e) { //redirect to Log in retry URL String url = ConfigurationFacade.getInstance().getAuthenticationEndpointRetryURL(); try { response.sendRedirect(url); throw new AuthenticationFailedException( "Invalid JSON object returned " + "from the Discovery Server", e); } catch (IOException e1) { throw new AuthenticationFailedException("response redirection failed.", e1); } } //get scope from authentication Properties or from the response in the Discovery API String scope = getMobileConnectScope(authenticatorProperties, operatoridScope); //get acr values from the authentication properties String acrValues = authenticatorProperties .get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ACR_VALUES); //retrieve current state String state = context.getContextIdentifier() + "," + MobileConnectAuthenticatorConstants.MOBILE_CONNECT_LOGIN_TYPE; //create oAuthClientrequest to contact the Authorization Endpooint try { OAuthClientRequest oAuthClientRequest; //set subscriberId if off-net if (!("true").equals(uiPromptStatus)) { oAuthClientRequest = OAuthClientRequest.authorizationLocation(authorizationEndpoint) .setClientId(authorizationClientId).setRedirectURI(getCallbackUrl(authenticatorProperties)) .setResponseType( MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_RESPONSE_TYPE) .setScope(scope).setState(state) .setParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ACR_VALUES, acrValues) .setParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_NONCE, state) .setParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_LOGIN_HINT, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_ENCR_MSISDN + ":" + subscriberId) .buildQueryMessage(); //Do not set subscriberId if off-net } else { oAuthClientRequest = OAuthClientRequest.authorizationLocation(authorizationEndpoint) .setClientId(authorizationClientId).setRedirectURI(getCallbackUrl(authenticatorProperties)) .setResponseType( MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_RESPONSE_TYPE) .setScope(scope).setState(state) .setParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ACR_VALUES, acrValues) .setParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_NONCE, state) .buildQueryMessage(); } //contact the authorization endpoint String url = oAuthClientRequest.getLocationUri(); response.sendRedirect(url); //set the context values to be used in the rest of the flow context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_ENDPOINT); context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_ENDPOINT, tokenEndpoint); context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_USERINFO_ENDPOINT, userinfoEndpoint); context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_CLIENT_ID, authorizationClientId); context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_CLIENT_SECRET, authorizationSecret); } catch (OAuthSystemException | IOException e) { throw new AuthenticationFailedException("response redirection failed", e); } } /** * Read the discovery API endpoint connections. */ private void discoveryEndpointRead(AuthenticationContext context, HttpServletResponse response, HttpURLConnection connection) throws JSONException, AuthenticationFailedException { try { //get response from HttpURLConnection int responseCode = connection.getResponseCode(); //if 200 OK if (responseCode == 200) { //read the response sent by the server BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } String responseString = stringBuilder.toString(); reader.close(); //create JSON object using the String retrieved JSONObject jsonObject = new JSONObject(responseString); //add the JSON object to the context of the session context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_JSON_OBJECT, jsonObject); //let the context know the end of the Discovery Process, and ste it to Authorization context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ENDPOINT); log.info("MSISDN is valid. Discovery Endpoint authorization successful"); } else if (responseCode == 302) { //if 302, move temporarily String redirectUrl = connection.getHeaderField("location"); //retrieve the redirect URL and redirect the flow response.sendRedirect(redirectUrl); context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_FLOW_STATUS, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ENDPOINT); log.info("MSISDN is invalid. Redirecting to mobile connect interface"); } else if (responseCode == 401) { //if 401 unauthorized String retryURL = ConfigurationFacade.getInstance().getAuthenticationEndpointRetryURL(); //redirect to retry URL response.sendRedirect(retryURL); log.error("No Authorization or Bad Session"); } else if (responseCode == 404 || responseCode == 400) { //if 404, not found String retryURL = ConfigurationFacade.getInstance().getAuthenticationEndpointRetryURL(); //redirect to retry URL response.sendRedirect(retryURL); log.error("Bad MSISDN is supplied"); } } catch (IOException e) { throw new AuthenticationFailedException("response redirection failed", e); } } /** * Return the mobile connect key from configuration files or UI. */ private String getMobileConnectAPIKey(Map<String, String> authenticatorProperties) throws AuthenticationFailedException { //retrieve mobile connect key from the configuration file of IS String mobileConnectKey = getAuthenticatorConfig().getParameterMap() .get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_KEY); if (StringUtils.isNotEmpty(mobileConnectKey)) { return mobileConnectKey; } else if (StringUtils.isNotEmpty( authenticatorProperties.get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_API_KEY))) { //retrieve the mobile connect key from the IS user interface return authenticatorProperties.get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_API_KEY); } else { //if both the configuration files and UI key values are null throw new AuthenticationFailedException("MobileConnect Key is not configured"); } } /** * Return the mobile connect scope from UI or Discovery API response. */ private String getMobileConnectScope(Map<String, String> authenticatorProperties, String operatoridScope) throws AuthenticationFailedException { //retrieve mobile connect scope from the UI if (StringUtils.isNotEmpty(authenticatorProperties .get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_SCOPE))) { return authenticatorProperties .get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_SCOPE); } else if (StringUtils.isNotEmpty(operatoridScope)) { //retrieve the mobile connect scope from the discovery response return operatoridScope; } else { //if both the UI scope and Discovery response scope values are null throw new AuthenticationFailedException("MobileConnect Scope is not configured correctly"); } } /** * Return the mobile connect secret from configuration files or UI. */ private String getMobileConnectAPISecret(Map<String, String> authenticatorProperties) throws AuthenticationFailedException { //retrieve mobile connect secret from the configuration file of IS String mobileConnectSecret = getAuthenticatorConfig().getParameterMap() .get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_SECRET); if (StringUtils.isNotEmpty(mobileConnectSecret)) { return mobileConnectSecret; } else if (StringUtils.isNotEmpty( authenticatorProperties.get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_API_SECRET))) { //retrieve the mobile connect secret from the IS user interface return authenticatorProperties.get(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_API_SECRET); } else { //if both the configuration files and UI secret values are null throw new AuthenticationFailedException("MobileConnect Secret is not configured"); } } /** * Process the response of the Discovery and Mobile Connect API and contact the Token Endpoint. */ private void tokenAuthenticationRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { //get authentication properties from the context Map<String, String> authenticatorProperties = context.getAuthenticatorProperties(); //get the following values from the context of the flow String tokenEndpoint = (String) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_ENDPOINT); String authorizationClientId = (String) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_CLIENT_ID); String authorizationSecret = (String) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_CLIENT_SECRET); try { String redirectURL = getCallbackUrl(authenticatorProperties); //get code sent back by the Token Endpoint String code = request.getParameter(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_CODE); //Base 64 encode the key and secret to attach as the header for URL connections String userPass = authorizationClientId + ":" + authorizationSecret; String authorizationHeader = "Basic " + Base64Utils.encode(userPass.getBytes(StandardCharsets.UTF_8)); //encode query parameters redirectURL = URLEncoder.encode(redirectURL, String.valueOf(StandardCharsets.UTF_8)); //prepare query parameters String queryParameters = "grant_type=" + MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_GRANT_TYPE + "&redirect_uri=" + redirectURL; //url with query parameters for TokenEndpoint API call String url = tokenEndpoint + "?" + "code=" + code + "&" + queryParameters; URL obj = new URL(url); HttpURLConnection connection = (HttpURLConnection) obj.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty( MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_AUTHORIZATION, authorizationHeader); connection.setRequestProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_CONTENT_TYPE, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_CONTENT_TYPE_VALUE); //if the connection is unauthorized if (connection.getResponseCode() == 401 || connection.getResponseCode() == 400) { String retryURL = ConfigurationFacade.getInstance().getAuthenticationEndpointRetryURL(); try { response.sendRedirect(retryURL); throw new AuthenticationFailedException("Invalid Token. Authentication failed"); } catch (IOException e1) { throw new AuthenticationFailedException("response redirection failed", e1); } //if connection is authorized } else { //read the response sent by the server BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } String responseString = stringBuilder.toString(); reader.close(); //create json object JSONObject jsonObject = new JSONObject(responseString); context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_JSON_OBJECT, jsonObject); log.info("Token Endpoint API call successful"); //call the user info Authentication Endpoint to retrieve user information userInfoAuthenticationRequest(context); } } catch (IOException | JSONException e) { throw new AuthenticationFailedException("Authentication failed", e); } } /** * Access the userinfo Endpoint and using the access_token. */ private void userInfoAuthenticationRequest(AuthenticationContext context) throws AuthenticationFailedException, IOException { BufferedReader bufferedReader = null; StringBuilder stringBuilder; try { //retrieve the user info endpoint url from the Context String url = getUserInfoEndpointURL(context); JSONObject jsonObject = (JSONObject) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_TOKEN_JSON_OBJECT); //decode JSON Object String accessToken = jsonObject.getString(MobileConnectAuthenticatorConstants.ACCESS_TOKEN); String tokenType = jsonObject.getString(MobileConnectAuthenticatorConstants.TOKEN_TYPE); String idToken = jsonObject.getString(MobileConnectAuthenticatorConstants.ID_TOKEN); //encode query parameters accessToken = URLEncoder.encode(accessToken, String.valueOf(StandardCharsets.UTF_8)); tokenType = URLEncoder.encode(tokenType, String.valueOf(StandardCharsets.UTF_8)); idToken = URLEncoder.encode(idToken, String.valueOf(StandardCharsets.UTF_8)); //prepare query parameters String queryParameters = "access_token=" + accessToken + "&token_type=" + tokenType + "&id_token=" + idToken; //add query parameters url = url + "?" + queryParameters; HttpGet httpGet = new HttpGet(url); String tokenValue = "Bearer " + accessToken; //add header values required httpGet.addHeader(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_AUTHORIZATION, tokenValue); //connect to the user info endpoint HttpResponse urlResponse = connectURL_get(httpGet); bufferedReader = new BufferedReader( new InputStreamReader(urlResponse.getEntity().getContent(), StandardCharsets.UTF_8)); stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line).append("\n"); } //this json String includes the mobile number. String jsonString = stringBuilder.toString(); JSONObject jsonUserInfo = new JSONObject(jsonString); context.setProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_USER_INFO_JSON_OBJECT, jsonUserInfo); } catch (IOException | JSONException e) { throw new AuthenticationFailedException("Authentication Error when contacting the userinfo endpoint", e); } finally { if (bufferedReader != null) { bufferedReader.close(); } } } /** * Get user info endpoint URL. */ private String getUserInfoEndpointURL(AuthenticationContext context) throws AuthenticationFailedException { String url = (String) context .getProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_USERINFO_ENDPOINT); if (StringUtils.isNotEmpty(url)) { return url; } else { throw new AuthenticationFailedException("User Info Endpoint not found"); } } /** * Execute the URL Get request. */ private HttpResponse connectURL_get(HttpGet request) throws IOException { //create client while disabling Redirect handling CloseableHttpClient client = HttpClientBuilder.create().disableRedirectHandling().build(); return client.execute(request); } /** * Call the Operator Selection UI of the Discovery Endpoint. */ private HttpResponse operatorSelectionDiscoveryCall(String authorizationHeader) throws IOException { //prepare query parameters String queryParameters = MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_REDIRECT_URL + "=" + MobileConnectAuthenticatorConstants.MOBILE_CONNECT_CALLBACK_URL; //url to call the Discovery API endpoint for operator selection URL String url = MobileConnectAuthenticatorConstants.DISCOVERY_API_URL + "?" + queryParameters; HttpGet httpGet = new HttpGet(url); //add header values required httpGet.addHeader(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_AUTHORIZATION, authorizationHeader); httpGet.addHeader(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ACCEPT, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ACCEPT_VALUE); //connect to the user info endpoint return connectURL_get(httpGet); } /** * msisdn based Discovery (Developer app uses Discovery API to send msisdn). */ private HttpURLConnection discoveryProcess(String authorizationHeader, String msisdn, Map<String, String> authenticationProperties) throws IOException { //prepare query parameters String queryParameters = MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_REDIRECT_URL + "=" + getCallbackUrl(authenticationProperties); //create url to make the API call String url = MobileConnectAuthenticatorConstants.DISCOVERY_API_URL + "?" + queryParameters; //body parameters for the API call String data = "MSISDN=" + msisdn; URL obj = new URL(url); HttpURLConnection connection = (HttpURLConnection) obj.openConnection(); connection.setRequestMethod(MobileConnectAuthenticatorConstants.POST_METHOD); connection.setRequestProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_AUTHORIZATION, authorizationHeader); connection.setRequestProperty(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ACCEPT, MobileConnectAuthenticatorConstants.MOBILE_CONNECT_DISCOVERY_ACCEPT_VALUE); connection.setDoOutput(true); //write data to the body of the connection DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); outputStream.writeBytes(data); outputStream.close(); return connection; } /** * Build the claims required to follow up the process. */ private void buildClaims(AuthenticationContext context, String jsonString) throws ApplicationAuthenticatorException { Map<String, Object> userClaims; userClaims = JSONUtils.parseJSON(jsonString); if (userClaims != null) { Map<ClaimMapping, String> claims = new HashMap<>(); for (Map.Entry<String, Object> entry : userClaims.entrySet()) { claims.put(ClaimMapping.build(entry.getKey(), entry.getKey(), null, false), entry.getValue().toString()); if (log.isDebugEnabled() && IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.USER_CLAIMS)) { log.debug("Adding claim mapping : " + entry.getKey() + " <> " + entry.getKey() + " : " + entry.getValue()); } } if (StringUtils .isBlank(context.getExternalIdP().getIdentityProvider().getClaimConfig().getUserClaimURI())) { context.getExternalIdP().getIdentityProvider().getClaimConfig() .setUserClaimURI(MobileConnectAuthenticatorConstants.CLAIM_ID); } String subjectFromClaims = FrameworkUtils .getFederatedSubjectFromClaims(context.getExternalIdP().getIdentityProvider(), claims); if (subjectFromClaims != null && !subjectFromClaims.isEmpty()) { AuthenticatedUser authenticatedUser = AuthenticatedUser .createFederateAuthenticatedUserFromSubjectIdentifier(subjectFromClaims); context.setSubject(authenticatedUser); } else { setSubject(context, userClaims); } context.getSubject().setUserAttributes(claims); } else { if (log.isDebugEnabled()) { log.debug("Decoded json object is null"); } throw new ApplicationAuthenticatorException("Decoded json object is null"); } } /** * Set the subject of the Authenticator in the context. */ private void setSubject(AuthenticationContext context, Map<String, Object> jsonObject) throws ApplicationAuthenticatorException { String authenticatedUserId = String .valueOf(jsonObject.get(MobileConnectAuthenticatorConstants.DEFAULT_USER_IDENTIFIER)); if (log.isDebugEnabled()) { log.debug("The subject claim that you have selected is null. The default subject claim " + authenticatedUserId + " has been set"); } if (StringUtils.isEmpty(authenticatedUserId)) { throw new ApplicationAuthenticatorException("Authenticated user identifier is empty"); } AuthenticatedUser authenticatedUser = AuthenticatedUser .createFederateAuthenticatedUserFromSubjectIdentifier(authenticatedUserId); context.setSubject(authenticatedUser); } /** * Get the Friendly Name of the Authenticator. */ @Override public String getFriendlyName() { return MobileConnectAuthenticatorConstants.AUTHENTICATOR_FRIENDLY_NAME; } /** * Get the Name of the Authenticator. */ @Override public String getName() { return MobileConnectAuthenticatorConstants.AUTHENTICATOR_NAME; } /** * Get the CallBackURL. */ @Override protected String getCallbackUrl(Map<String, String> authenticatorProperties) { if (StringUtils.isNotEmpty(authenticatorProperties.get(IdentityApplicationConstants.OAuth2.CALLBACK_URL))) { return authenticatorProperties.get(IdentityApplicationConstants.OAuth2.CALLBACK_URL); } return MobileConnectAuthenticatorConstants.MOBILE_CONNECT_CALLBACK_URL; } /** * Get Configuration Properties. */ @Override public List<Property> getConfigurationProperties() { List<Property> configProperties = new ArrayList<>(); //set the mobile connect Authentication type Property authenticationType = new Property(); authenticationType.setName(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHENTICATION_TYPE); authenticationType.setDisplayName("Mobile Connect Authentication Type"); authenticationType.setRequired(false); authenticationType.setValue("on-net"); authenticationType.setDescription("Type 'on-net' or 'off-net' to use relevant authentication type"); authenticationType.setDisplayOrder(0); configProperties.add(authenticationType); //set the mobile connect key input field Property mobileConnectKey = new Property(); mobileConnectKey.setName(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_API_KEY); mobileConnectKey.setDisplayName("Mobile Connect Key"); mobileConnectKey.setRequired(false); mobileConnectKey.setConfidential(true); mobileConnectKey.setDescription("Enter the Mobile Connect Key of the Mobile Connect Application Account"); mobileConnectKey.setDisplayOrder(1); configProperties.add(mobileConnectKey); //set the mobile connect secret input field Property mobileConnectSecret = new Property(); mobileConnectSecret.setName(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_API_SECRET); mobileConnectSecret.setDisplayName("Mobile Connect Secret"); mobileConnectSecret.setRequired(false); mobileConnectSecret.setConfidential(true); mobileConnectSecret .setDescription("Enter the Mobile Connect Secret of the Mobile Connect Application Account"); mobileConnectSecret.setDisplayOrder(2); configProperties.add(mobileConnectSecret); //set the mobile connect scope input field Property mobileConnectScope = new Property(); mobileConnectScope.setName(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_SCOPE); mobileConnectScope.setDisplayName("Mobile Connect Scope"); mobileConnectScope.setRequired(true); mobileConnectScope.setValue("openid"); mobileConnectScope.setDescription("Enter the Mobile Connect Scope Required"); mobileConnectScope.setDisplayOrder(3); configProperties.add(mobileConnectScope); //set the mobile connect arc values Property mobileConnectAcrValues = new Property(); mobileConnectAcrValues.setName(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_AUTHORIZATION_ACR_VALUES); mobileConnectAcrValues.setDisplayName("Mobile Connect ACR Values"); mobileConnectAcrValues.setRequired(true); mobileConnectAcrValues.setValue("2"); mobileConnectAcrValues.setDescription("Enter the Mobile Connect ACR Values required"); mobileConnectAcrValues.setDisplayOrder(4); configProperties.add(mobileConnectAcrValues); //set the mobile connect redirect URL Property mobileConnectCallBack = new Property(); mobileConnectCallBack.setName(IdentityApplicationConstants.OAuth2.CALLBACK_URL); mobileConnectCallBack.setDisplayName("Mobile Connect Redirect URL"); mobileConnectCallBack.setRequired(false); mobileConnectAcrValues.setValue(MobileConnectAuthenticatorConstants.MOBILE_CONNECT_CALLBACK_URL); mobileConnectCallBack.setDescription("Enter the Mobile Connect Redirect URL"); mobileConnectCallBack.setDisplayOrder(5); configProperties.add(mobileConnectCallBack); return configProperties; } }