OpenLiberty / open-liberty

Open Liberty is a highly composable, fast to start, dynamic application server runtime environment
https://openliberty.io
Eclipse Public License 2.0
1.13k stars 575 forks source link

@OpenIdAuthenticationMechanismDefinition not working #28399

Open barbj opened 3 weeks ago

barbj commented 3 weeks ago

Describe the bug
Using the annotation OpenIdAuthenticationMechanismDefinition is documented here: https://openliberty.io/docs/latest/enable-openid-connect-client.html

An app is developed including:

@[trace.log](https://github.com/OpenLiberty/open-liberty/files/15271206/trace.log)(
                    providerURI = "https://localhost:9443/oidc/endpoint/OP",
                    clientId = "oidcclient",
                    clientSecret = "password",
                    //redirectToOriginalResource = true)
                    redirectURI = "https://localhost:9444/simple/Callback")
@DeclareRoles("all")

This is emitted for the request:

CWOAU0056E: The redirect URI parameter [https://localhost:9444/simple/Callback] provided in the OAuth or OpenID Connect request did not match any of the redirect URIs registered with the OAuth provider [https://localhost:9444/oidcclient/redirect/RP].

The Liberty doc describes that:

To use the minimal annotation, the application needs to provide an implementation for the callback servlet. This callback servlet must use the OpenIdContext API to retrieve the original request URL and then redirect to the original resource, as shown in the following example.

If you want to use a callback servlet, you do have to implement it and include it in an application deployed to the Liberty server.


With your app and config, I was able to get past any issues with the redirect URI simply by adding https://localhost:9444/simple/Callback to the client configuration in the OP server:

    <oauthProvider id="Oauth" jwtAccessToken="true">
        <localStore>
            <client name="oidcclient"  secret="password" scope="openid" redirect="https://localhost:9444/oidcclient/redirect/RP,https://localhost:9444/simple/Callback"/>
        </localStore>
    </oauthProvider>

However I get stuck in a loop going to the scope consent page; the OP asks if oidcclient can access the openid scope. When I click one of the allow buttons, it seems the login flow starts anew, with the browser being redirected to the RP’s callback servlet, but then redirected back to the OP’s authorization endpoint to log in. I’m already logged in, so I get the consent page all over again.

Looks like that’s happening because somehow when this line is invoked in the callback servlet:

Optional originalRequest = context.getStoredValue(request, response, OpenIdConstant.ORIGINAL_REQUEST);

The context object ends up trying to look for the original request URL in a cookie, even though the OIDC client originally stored the value in the HTTP session. Since the context can’t find the value, it can’t complete its processing correctly. If I explicitly set

useSession = false

in the @OpenIdAuthenticationMechanismDefinition I can get a little further. But then I get problems validating the JWT signature because the OP is signing the tokens using HS256 by default but the client expects RS256.


Adam: I still have to investigate why the OpenIdContext object is looking in the cookies and not the HTTP session.


Diagnostic information:

Original server.xml file:

<server description="Liberty_OP">
    <featureManager>
        <feature>openidConnectServer-1.0</feature>
       <feature>localConnector-1.0</feature>
     <feature>jakartaee-10.0</feature> 
    </featureManager>
    <httpEndpoint httpPort="9080" httpsPort="9443" id="defaultHttpEndpoint"/>
    <keyStore id="defaultKeyStore" password="Password"/>
        <authentication cacheEnabled="false"/>
    <basicRegistry>
        <user name="Jackson" password="xxxxx"/>
        <user name="Andrea" password="xxxxx"/>
    </basicRegistry>
    <openidConnectProvider id="OP" oauthProviderRef="Oauth"/>
    <oauthProvider id="Oauth" jwtAccessToken="true">
        <localStore>
            <client name="oidcclient"  secret="xxxxx" scope="openid" redirect="https://localhost:9444/oidcclient/redirect/RP"/>
        </localStore>
    </oauthProvider>
    <oauth-roles>
        <authenticated>
            <special-subject type="ALL_AUTHENTICATED_USERS"/>
        </authenticated>
    </oauth-roles>
</server>

Implemented callback:

package com.ibm.cics.openid.Jakarta10.app;

import java.io.IOException;

import jakarta.annotation.security.DeclareRoles;
import jakarta.security.enterprise.authentication.mechanism.http.OpenIdAuthenticationMechanismDefinition;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.HttpConstraint;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/Oidc")
@OpenIdAuthenticationMechanismDefinition(
                    providerURI = "https://localhost:9443/oidc/endpoint/OP",
                    clientId = "oidcclient",
                    clientSecret = "password",
                    redirectToOriginalResource = true)
                   // redirectURI = "https://localhost:9444/oidcclient/redirect/RP")
@DeclareRoles("all")
@ServletSecurity(@HttpConstraint(rolesAllowed = "all"))
public class OidcAnnotatedServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)  throws ServletException, IOException {
        System.out.println("HE");
        response.getWriter().println("Hello MY Friend");
    }

}
ayoho commented 1 week ago

I was able to successfully reach the protected servlet after doing a few updates:

  1. @OpenIdAuthenticationMechanismDefinition updates:
    • Add useSession = false
      • I still have to investigate this part.
    • Add claimsDefinition = @ClaimsDefinition(callerNameClaim = "sub")
      • By default we look for the preferred_username claim to use as the caller name. Neither the access token, ID token, or UserInfo data included this claim, so we have to tell the annotation to look at a claim that is in one of those places.
  2. OP server.xml updates:
    • Add signatureAlgorithm="RS256" to <openidConnectProvider>
      • The Liberty OP signs tokens using HS256 by default. I would think the Liberty RP configured via @OpenIdAuthenticationMechanismDefinition should be able to work just fine with HS256, but for whatever reason it isn't. It's possible we have a bug here; this is another thing for me to investigate a bit more.
    • Add jwkEnabled="true" to <openidConnectProvider>
      • Helps when the OP signs using RS256 to avoid having to update keystores in the RP; this way the RP can dynamically fetch the signing key it needs.
  3. RP server.xml updates:
    • Add "all" security-role to application-bnd:
      <security-role name="all">
          <special-subject type="ALL_AUTHENTICATED_USERS" />
      </security-role>
      • The protected servlet declares the "all" role and limits access just to users in that role. At the moment the application-bnd definition in the RP only maps all authenticated users to the "OIDCUser" role, so you'd get a 403 trying to access the servlet because the user isn't mapped to the "all" role.
ayoho commented 1 week ago

I provide that update just to point out the things I did within the customer's control to get things working. As I mention there, I still need to investigate why I had to add useSession = false to the @OpenIdAuthenticationMechanismDefinition annotation and why there's a mismatch between looking in cookies or looking in the session for cached data.