Saturday, April 16, 2016

Federated Authenticator in WSO2 Identity Server 5.1.0

An authenticator allows you to authenticate the user using third party authentication systems such as LinkedIn, Clef and Foursquare. You can configure the authenticators for your identity provider to reach out to third party authentication systems to authenticate a user who logs in to your application. For example, if you configure LinkedIn authenticator as your authenticator in WSO2 Identity Server, you can authenticate a user of your application using LinkedIn authentication. You can also do the authentication with multiple third party authentication systems with a single authentication request called multifactor authentication .

In his post i will describe step by step how wso2 is Amazon authenticator works. before start this explanation I’m assuming everyone know to create a custom federated authenticator for wso2 is 5.1.0 if not please visit and learn how.
 
 This image illustrate how OAuth 2 flow works generally,  Now let's look deep look on WSO2IS how it's handling the flow by explaining code

Authorization Request 

To request authorization, the client (website) must redirect the user-agent (browser) to make a secure HTTP call to https://www.amazon.com/ap/oa with the following parameters:
  • client_id REQUIRED. The client identifier. This is set when you register your website as a client. For more information, see Client Identifier (p. 11).
  • scope REQUIRED. The scope of the request. Must be profile, profile:user_id, postal_code, or some combination, separated by spaces (e.g. profile%20postal code). For more information, see Customer Profile.
  • response_type REQUIRED. The type of response requested. Must be code for this scenario.
  • redirect_uri REQUIRED. The HTTPS address where the authorization service should redirect the user.
  • state RECOMMENDED. An opaque value used by the client to maintain state between this request and the response. The authorization service will include this value when redirecting the user back to the client. It is also used to prevent cross-site request forgery. For more information see Cross-site Request Forgery.
 example: https://www.amazon.com/ap/oa?client_id=foodev&scope=profile&response=typecode&state=208257577ll0975l93l2l59l895857093449424&redirect_uri=https://client.example.com/auth_popup/token

These request in IS handle by initiateAuthenticationRequest method from OpenIDConnectAuthenticator. Here we are creating request by setting values for required parameters. Since it's have all other parameters we need to add scope from our class method call getScope.
    @Override
    protected String getScope(String scope, Map<String, String> authenticatorProperties) {
        return AmazonAuthenticatorConstants.AMAZON_SCOPE_PROFILE;
    }
 This will set scope to our request in amazon request our scope will be Profile.

Once you successfully done the work it's mean set the scope in getScope method you will get the bellow page successfully.
Once you login Successfully then you will get the below window to  get the user confirmation to sent the grant.

Authorization Grand(Resource Owner)

 After the client directs the user-agent to make an Authorization Request, the authorization service will redirect the user-agent to a URI specified by the client. If the user granted the request for access, that URI will contain a code parameter containing the authorization code. An authorization code in Amazon is valid for 6 hours. But it's depend on the sever.

The redirect also copies the state passed by the user-agent in the authorization request. This value allows you to keep track of the user's state before the request. It is also used to prevent cross-site request forgery.

HTTP/l.l 302 Found
Location: https://client.example.com/cb?code=SplxlOBezQQYbYS6WxSbIA&state=208257577ll0975l93l2l59l895857093449424

When WSO2IS receive the response it will fall into canHandle() method. Normally this method was written in OpenIDConnectAuthenticator This will check the response whether the response contain the code and state or not and if checking is pass it will call processAuthenticationResponse method for further process.

@Override
    protected void processAuthenticationResponse(HttpServletRequest request, HttpServletResponse response,
                                                 AuthenticationContext context)
            throws AuthenticationFailedException {
        try {
            Map<String, String> authenticatorProperties = context.getAuthenticatorProperties();
            String clientId = authenticatorProperties.get(OIDCAuthenticatorConstants.CLIENT_ID);
            String clientSecret = authenticatorProperties.get(OIDCAuthenticatorConstants.CLIENT_SECRET);
            String tokenEndPoint = getTokenEndpoint(authenticatorProperties);
            String callbackUrl = getCallbackUrl(authenticatorProperties);
            OAuthAuthzResponse authorizationResponse = OAuthAuthzResponse.oauthCodeAuthzResponse(request);
            String code = authorizationResponse.getCode();
            OAuthClientRequest accessRequest =
                    getAccessRequest(tokenEndPoint, clientId, code, clientSecret, callbackUrl);
            OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
            OAuthClientResponse oAuthResponse = getOauthResponse(oAuthClient, accessRequest);
            String accessToken = oAuthResponse.getParam(OIDCAuthenticatorConstants.ACCESS_TOKEN);
            if (StringUtils.isBlank(accessToken)) {
                throw new AuthenticationFailedException("Access token is empty");
            }
            context.setProperty(OIDCAuthenticatorConstants.ACCESS_TOKEN, accessToken);
            Map<ClaimMapping, String> claims;
            AuthenticatedUser authenticatedUserObj;
            String json = sendRequest(AmazonAuthenticatorConstants.AMAZON_USERINFO_ENDPOINT,
                    oAuthResponse.getParam(OIDCAuthenticatorConstants.ACCESS_TOKEN));
            JSONObject jsonObject = new JSONObject(json);
            authenticatedUserObj = AuthenticatedUser.createFederateAuthenticatedUserFromSubjectIdentifier(
                    (String) jsonObject.get(AmazonAuthenticatorConstants.USER_ID));
            authenticatedUserObj
                    .setAuthenticatedSubjectIdentifier((String) jsonObject.get(AmazonAuthenticatorConstants.USER_ID));
            claims = getSubjectAttributes(oAuthResponse, authenticatorProperties);
            authenticatedUserObj.setUserAttributes(claims);
            context.setSubject(authenticatedUserObj);
        } catch (OAuthProblemException | IOException | JSONException e) {
            throw new AuthenticationFailedException("Authentication process failed " + e.getMessage(), e);
        }
    }

From the response we will get the code in the code level for next request.

            OAuthAuthzResponse authorizationResponse = OAuthAuthzResponse.oauthCodeAuthzResponse(request);
            String code = authorizationResponse.getCode();

Authorization Grand(Client)

 Once the client receives an Authorization Response with a valid authorization code, it can use that code to obtain an access token. With an access token, the client can read a customer profile. To request an access token, the client makes a secure HTTP POST to https://api.amazon.com/auth/o2/token with the following parameters:
  • grant_type REQUIRED. The type of access grant requested. Must be Authorization_code.
  • code REQUIRED. The code returned by the authorization request.
  • redirect_uri REQUIRED. If you provided a redirect_uri for the authorization request, you must pass the same redirect_uri here. If you used the Login with Amazon SDK for JavaScript for the authorization request, you do not need to pass a redirect_uri here.
  • client_id REQUIRED. The client identifier. This is set when you register your website as a client. For more information, see Client Identifier.
  • client_secret REQUIRED. The secret value assigned to the client during registration.
POST /auth/o2/token HTTP/l.l
Host: api.amazon.com
Content-Type: application/x-www-form-urlencoded;charset=UTF-8


grant_type=authorization_code&code=SplxlOBezQQYbYS6WxSbIA&client_id=foodev&client_secret=Y76SDl2F

This is also handle by processAuthenticationResponse method
            OAuthClientRequest accessRequest = getAccessRequest(tokenEndPoint, clientId, code, clientSecret, callbackUrl);
            OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
            OAuthClientResponse oAuthResponse = getOauthResponse(oAuthClient, accessRequest);


private OAuthClientRequest getAccessRequest(String tokenEndPoint, String clientId, String code,
                                                String clientSecret, String callbackurl)
            throws AuthenticationFailedException {
        OAuthClientRequest accessRequest;
        try {
            accessRequest = OAuthClientRequest.tokenLocation(tokenEndPoint)
                    .setGrantType(GrantType.AUTHORIZATION_CODE)
                    .setCode(code)
                    .setRedirectURI(callbackurl)
                    .setClientId(clientId)
                    .setClientSecret(clientSecret)
                    .buildBodyMessage();
        } catch (OAuthSystemException e) {
            throw new AuthenticationFailedException("Exception while building request " +
                    "for request access token", e);
        }
        return accessRequest;
    }

Here we are setting values to parameters for request.

private OAuthClientResponse getOauthResponse(OAuthClient oAuthClient, OAuthClientRequest accessRequest)
            throws AuthenticationFailedException {
        OAuthClientResponse oAuthResponse;
        try {
            oAuthResponse = oAuthClient.accessToken(accessRequest);
        } catch (OAuthProblemException | OAuthSystemException e) {
            throw new AuthenticationFailedException("OAuthProblem Exception while " +
                    "requesting access token", e);
        }
        return oAuthResponse;
    }
This will sent the request to Authorization server fro Access Token.

Access Token(Authorization Server)

When a client makes a secure HTTP POST Authorization Request, the authorization server immediately returns the access token or an error in the HTTP response. Response parameters are encoded using the application/json media type.

For example:

HTTP/l.l 200 OK
Content-Type: application/json;charset UTF-8
Cache-Control: no-store
Pragma: no-cache


{
     "access_token":"Atza|IQEBLjAsAhRmHjNgHpi0U-Dme37rR6CuUpSR...",
     "token_type":"bearer",
     "expires_in":3600,
     "refresh_token":"Atzr|IQEBLzAtAhRPpMJxdwVz2Nn6f2y-tpJX2DeX..."
}

  • access_token The access token for the user account.
  • token_type The type of token returned. Should be bearer.
  • expires_in The number of seconds before the access token becomes invalid.
  • refresh_token A refresh token that can be used to request a new access token.
  • scope The scope of the request. Must be profile, profile:user_id
  • postal_code, or some combination.
String accessToken = oAuthResponse.getParam(OIDCAuthenticatorConstants.ACCESS_TOKEN);

Authenticator will receive the access token from authorization sever and will store for the rest of the service.

Access Token(Client)

 Once the user grants your website access to their Amazon customer profile, you will receive an access token. To access the authorized customer data, you submit that access token to Login with Amazon using HTTPS. In response, Login with Amazon will return the appropriate customer profile data. The profile data you receive is determined by the scope you specified when requesting access. The access token reflects access permission for that scope.

The request to Resource Server should be in below format

GET /user/profile HTTP/l.l
Host: api.amazon.com
Date: Wed, 0l Jun 20ll l2:00:00 GMT
Authorization: Bearer Atza|IQEBLjAsAhRmHjNgHpi0U-Dme37rR6CuUpSR...
Accept: application/json
Accept-Language: en-US


Here in authenticator we can use our saved access token to sent this request through sendRequest method to achieve this we need to pass endpoint and access token to sendRequest method.  If we need different function we can Overwrite the method.

String json = sendRequest(AmazonAuthenticatorConstants.AMAZON_USERINFO_ENDPOINT,
                    oAuthResponse.getParam(OIDCAuthenticatorConstants.ACCESS_TOKEN));

Protected Resource

If your access token is valid, you will receive the customer's profile data as an HTTP response in JSON.
For example:

HTTP/l.l 200 OK
x-amzn-RequestId: 0f6bef6d-705c-lle2-aacb-93e6bf26930l
Content-Type: application/json
Content-Language: en-US
Content-Length: 85
{
    "user_id": "amznl.account.K2LI23KL2LK2",
    "email":"mhashimoto-04@plaxo.com",
    "name" :"Mork Hashimoto",
    "postal_code": "98052"
}


Authenticator can able to receive the response and need to change as JSONObject .
JSONObject jsonObject = new JSONObject(json);

Now, we successfully received client protected resorce from third party so remaining will be create a authenticated user in WSO2IS.

            authenticatedUserObj = AuthenticatedUser.createFederateAuthenticatedUserFromSubjectIdentifier(
                    (String) jsonObject.get(AmazonAuthenticatorConstants.USER_ID));
            authenticatedUserObj
                    .setAuthenticatedSubjectIdentifier((String) jsonObject.get(AmazonAuthenticatorConstants.USER_ID));
            claims = getSubjectAttributes(oAuthResponse, authenticatorProperties);
            authenticatedUserObj.setUserAttributes(claims);
            context.setSubject(authenticatedUserObj);

Finally, we will create a new federate authenticated user Object and get unique value from response for  createFederateAuthenticatedUserFromSubjectIdentifier
and need to set subject setAuthenticatedSubjectIdentifier and add claims in authenticatedUserObj after add authenticatedUserObj in AuthenticationContext.

Once you successfully complete above steps you will get an output like in below image .