14 December 2015

Fediz with OpenID Connect Support and WS-Federation Bridge (2/2)

Setup a Demonstrator

In this article I'll explain how to setup a demonstrator for the use case described in my previous post.

Setup Fediz IDP & OIDC

First you need to setup the Fediz IDP as usual. To get the OIDC Service working you also need to do the following:
  1. Install Fediz Plugin for the Fediz IDP Server (usually you would do this for the client application only)
    For the fediz_config.xml you can use the sample provided with the OIDC Service.
  2. Download or build the OIDC service and then deploy the fediz-oidc.war file to your webapps folder (same place where you deployed STS & IDP)

Register an OpenID Connect Client 

From the perspective of OpenID Connect the Web Portal takes the role of a OIDC client. So the client must be registered up front. After starting the OIDC service you can invoke the following URL in your browser:

https://localhost:9443/fediz-oidc/clients/register
For Client Name and Client Description you can enter any human readable and meaningful value related to your service.
OIDC supports Confidential as well as Public as a Client Type. If your Client is a Public client (for example your client is java code running inside the browser of a user), no client secret will be generated, since the secret could not be protected anyway. The normal use case however should be Confidential.
Your redirect URI will be the final URL from which the OIDC service will redirect the user with the generated code. Normally this will be a OIDC Client service from your app consuming the code value and exchanging it for an access and ID token.
If your application (OIDC Client) is  bound to a fix Home Realm (all users from this app will always login at the same home realm), then you can select this HomeRealm here. In this case users will not see a home realm selection screen but will be redirected to the correct home realm IDP directly.

After submitting you client information you will see the generated client_id and client_secret. These values need to be set at the web portal.

Understanding the Web Portal

Your web portal can by any kind of (java) web application. To enable OpenID Connect support at my web application I need to add a Relaying Party (RP) OIDC Handler at a URL of my choice.
<!-- 
OIDC RP endpoint: authenticates a user by redirecting a user to OIDC Provider, and redirects the user 
                  to the initial application form once the authentication is done
-->
<jaxrs:server id="oidcRpServer" address="/oidc">
    <jaxrs:serviceBeans>
        <bean class="org.apache.cxf.rs.security.oidc.rp.OidcRpAuthenticationService">
            <!-- This state manager is shared between this RP endpoint and the oidcRpFilter which protects
                 the application endpoint, the RP endpoint sets an OIDC context on it and the filter checks 
                 the context is available -->
            <property name="clientTokenContextManager" ref="stateManager" />
            <!-- Where to redirect to once the authentication is complete -->
            <property name="defaultLocation" value="/app/service/start" />
        </bean>
    </jaxrs:serviceBeans>
    
    <jaxrs:providers>
        <!-- the filter which does the actual work for obtaining an OIDC context.
             It redirect to the OIDC Provider, exchanges an authorization code for access token,
             extracts OIDC IdToken and makes it all available as OidcCientTokenContext
        -->
        <bean id="rpOidcRequestFilter" class="org.apache.cxf.rs.security.oidc.rp.OidcClientCodeRequestFilter">
            <property name="clientCodeStateManager">
                <!-- This state manager creates an OAuth2 'state' parameter and saves it in the HTTP session -->
                <bean class="org.apache.cxf.rs.security.oauth2.client.MemoryClientCodeStateManager">
                    <property name="generateNonce" value="true"/>
                </bean>
            </property>
            <property name="scopes" value="openid refreshToken" />
            <property name="accessTokenServiceClient" ref="atServiceClient" />
            <property name="idTokenReader">
                <bean class="org.apache.cxf.rs.security.oidc.rp.IdTokenReader">
                    <!-- disable it if the local key store or the client secret is used to validate ID Tokens -->
                    <property name="jwkSetClient" ref="jwkSetClient"/> 
                    <property name="issuerId" value="accounts.fediz.com"/>
                </bean>
            </property>
            <property name="consumer" ref="consumer" />
            <property name="authorizationServiceUri" value="https://localhost:9443/fediz-oidc/idp/authorize" />
            <property name="startUri" value="rp" />
            <property name="completeUri" value="/" />
        </bean>
   
        <!-- JAX-RS provider that makes OidcClientTokenContext available as JAX-RS @Context -->
        <ref bean="clientTokenContextProvider" />
    </jaxrs:providers>
</jaxrs:server>
 
<!-- The state manager shared between the RP and application endpoints -->
<bean id="stateManager" class="org.apache.cxf.rs.security.oauth2.client.MemoryClientTokenContextManager"/>
<!-- WebClient for requesting an OAuth2 Access token.
     rpOidcRequestFilter uses it to exchange a code for a token --> 
 
<jaxrsclient:client id="atServiceClient" threadSafe="true" 
   address="https://localhost:9443/fediz-oidc/oauth2/token"
   serviceClass="org.apache.cxf.jaxrs.client.WebClient">
   <jaxrsclient:headers>
       <entry key="Accept" value="application/json"/>
   </jaxrsclient:headers>
   <jaxrsclient:providers>
      <bean class="org.apache.cxf.jaxrs.provider.FormEncodingProvider">
          <property name="expectedEncoded" value="true"/>
      </bean> 
   </jaxrsclient:providers>
</jaxrsclient:client>

<!-- Client id and secret allocated by OIDC ClientRegistrationService -->
<bean id="consumer" class="org.apache.cxf.rs.security.oauth2.client.Consumer">
    <property name="clientId" value="-7TdKEwzkf5BSQ"/> 
    <property name="clientSecret" value="q6ys7349uXMIgOu1kXNFTQ"/>
</bean>     
 
<!-- JAX-RS provider that makes OidcClientTokenContext available as JAX-RS @Context -->
<bean id="clientTokenContextProvider" class="org.apache.cxf.rs.security.oauth2.client.ClientTokenContextProvider"/>

<!-- disable it if the local key store or the client secret is used to validate ID Tokens -->
<jaxrsclient:client id="jwkSetClient" threadSafe="true" 
   address="https://localhost:9443/fediz-oidc/jwk/keys"
   serviceClass="org.apache.cxf.jaxrs.client.WebClient">
   <jaxrsclient:headers>
       <entry key="Accept" value="application/json"/>
   </jaxrsclient:headers>
   <jaxrsclient:providers>
      <bean class="org.apache.cxf.rs.security.jose.jaxrs.JsonWebKeysProvider"/> 
   </jaxrsclient:providers>
</jaxrsclient:client>

For being able to use the access token when invoking a REST Backend-Service you must add the access token as an Authorization Header within your Request. For this purpose you must first create a JAX-RS Client and also inject the OIDC-Context to your application.
<jaxrsclient:client id="backendServiceClient" threadSafe="true" 
   address="https://localhost:8082/backendService" serviceClass="org.apache.cxf.jaxrs.client.WebClient">
   <jaxrsclient:headers>
       <entry key="Accept" value="application/json"/>
   </jaxrsclient:headers>
   <jaxrsclient:providers>
      <bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider"/>
   </jaxrsclient:providers>
</jaxrsclient:client>
Within your code you need to inject the OidcClientTokenContext, for example via @Context annotation:
@Context
private OidcClientTokenContext oidcContext;
Within your method when just before invoking the REST Service you must add the authorization header as follows:
ClientAccessToken accessToken = oidcContext.getToken();        
backendServiceClient.authorization(accessToken);

Understanding the REST Service

Lets take a DemoService as an example to explain the OIDC/Auth2 Integration at the backend REST Service.First lets take a loog a the REST Interface. You will note an annotation @Scopes which will require that the used access token was provided for the scope userinfo. The other method will also require an access token, but without any scope enforcement.
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.apache.cxf.rs.security.oauth2.filters.Scopes;

@Path("/")
public class DemoService {
    
    @GET
    @Path("/public")
    public String getGreeting() {
        return "Hello World!";
    }
    
    @GET
    @Path("/secure")
    @Scopes("userinfo")
    public String getPersonalGreeting(@QueryParam("name") String name) {
        return "Hello " + name + "!";
    }
}

Within your Spring configuration you need to add two filters. The OAuthRequestFilter is needed to ensure that the provided access token is still valid, and the OAuthScopesFilter checks that the required scope for invoking a certain method will matches with the scope of the access token.
<bean id="demoService" class="org.apache.service.DemoService"/>

<jaxrs:server id="demoService" address="/greeting">
    <jaxrs:serviceBeans>
        <ref bean="demoService"/>
    </jaxrs:serviceBeans>
    <jaxrs:providers>
        <bean class="org.apache.cxf.rs.security.oauth2.filters.OAuthRequestFilter">
            <property name="tokenValidator">
                <bean class="org.apache.cxf.rs.security.oauth2.filters.AccessTokenValidatorClient">
                    <property name="tokenValidatorClient" ref="tokenValidatorClient"/>
                </bean>
            </property>
        </bean>
        <bean class="org.apache.cxf.rs.security.oauth2.filters.OAuthScopesFilter">
            <property name="securedObject" ref="demoService"/>
        </bean>
    </jaxrs:providers>
</jaxrs:server>

<jaxrsclient:client id="tokenValidatorClient" 
    address="https://localhost:9443/fediz-oidc/oauth2/validate"
    serviceClass="org.apache.cxf.jaxrs.client.WebClient">
    <jaxrsclient:headers>
        <entry key="Content-Type" value="application/x-www-form-urlencoded"/>
        <entry key="Accept" value="application/xml"/>
    </jaxrsclient:headers>
    <jaxrsclient:providers>
       <bean class="org.apache.cxf.jaxrs.provider.FormEncodingProvider">
           <property name="expectedEncoded" value="true"/>
       </bean>
    </jaxrsclient:providers>
</jaxrsclient:client>
Since validation is not yet standardized  the tokenValidatorClient communicates with the OIDC service in a proprietary manner.

3 comments: