General documentation: Article directory Github: github.com/black-ant

A. The preface

Why Pac4j is a authentication tool? Because it really provides a full set of wrapper classes that allow most applications to be integrated quickly without the need for a relational authentication protocol process, users only need to request and acquire users

It should be noted that there are many different versions of Pac4j and their implementations differ greatly. My source code is based on 3.8.0, analyze its ideas, and then separately compare the optimization of subsequent versions, but not much in-depth source details

2. Basic use

One of the features of Pac4j is that it provides a wide range of clients for different vendors. Authentication processing is basically not customized, but here we try to customize our own process

Take OAuth for example:

2.1 Building an Authoriza request

Let’s build a Client to make a request:

OAuth20Client: The most native client invocation class. As you can see below, PAC4J has many custom client classes

public class OAuthService extends BasePac4jService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private final static String CLIENT_ID = "b45e4-41c0-demo";
    private final static String CLIENT_SECRET = "0407581-ef15-f773-demo";
    private final static String CALLBACK_URL = "http://127.0.0.1:8088/oauth/callback";

    /** * Execute Authorization request **@return* /
    public void doOAuthRequest(HttpServletRequest request, HttpServletResponse response) {

        // Step 1: Build the request Client
        OAuth20Configuration config = new OAuth20Configuration();
        config.setApi(new DefaultOAuthAPI());
        config.setProfileDefinition(new DefaultOAuthDefinition());
        config.setScope("user");
        config.setKey(CLIENT_ID);
        config.setSecret(CLIENT_SECRET);

        // Step 2: Build a Client
        OAuth20Client client = new OAuth20Client();

        // Add the perfect attribute
        client.setConfiguration(config);
        client.setCallbackUrl(CALLBACK_URL);

        // Step 3: Build the request, which is redirected via 302
        J2EContext context = new J2EContext(request, response);
        client.redirect(context);

        // Step 4: Cache data
        request.getSession().setAttribute("client", client); }}Copy the code

Note that there is a DefaultOAuthAPI and DefaultOAuthDefinition that defines the SSO path and Profile declaration

DefaultOAuthAPI

DefaultOAuthAPI mainly contains the requested address. DefaultApi20 has two abstract interfaces, and I added one of my own

The DefaultOAuthAPI is unrestricted and allows you to put any interface you want in it for subsequent fetching.

public class DefaultOAuthAPI extends DefaultApi20 {

    public String getRootEndpoint(a) {
        return "http://127.0.0.1/sso/oauth2.0/";
    }

    @Override
    public String getAccessTokenEndpoint(a) {
        return getRootEndpoint() + "accessToken";
    }

    @Override
    protected String getAuthorizationBaseUrl(a) {
        return getRootEndpoint() + "authorize"; }}Copy the code

DefaultOAuthDefinition

This declaration acts as a dictionary to translate the data returned by the profile

The entire class does the following:

  • Defines the attributes to be returned by the User profile
  • Various transformation classes and mappings are defined
  • Defines the address of the profile request
  • Defines the actual implementation of the transformed data

public class DefaultOAuthDefinition extends OAuth20ProfileDefinition<DefaultOAuhtProfile.OAuth20Configuration> {

    public static final String IS_FROM_NEW_LOGIN = "isFromNewLogin";
    public static final String AUTHENTICATION_DATE = "authenticationDate";
    public static final String AUTHENTICATION_METHOD = "authenticationMethod";
    public static final String SUCCESSFUL_AUTHENTICATION_HANDLERS = "successfulAuthenticationHandlers";
    public static final String LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED = "longTermAuthenticationRequestTokenUsed";

    public DefaultOAuthDefinition(a) {
        super(x -> new DefaultOAuhtProfile());
        primary(IS_FROM_NEW_LOGIN, Converters.BOOLEAN);
        primary(AUTHENTICATION_DATE, new DefaultDateConverter());
        primary(AUTHENTICATION_METHOD, Converters.STRING);
        primary(SUCCESSFUL_AUTHENTICATION_HANDLERS, Converters.STRING);
        primary(LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED, Converters.BOOLEAN);
    }

    @Override
    public String getProfileUrl(final OAuth2AccessToken accessToken, final OAuth20Configuration configuration) {
        return ((DefaultOAuthAPI) configuration.getApi()).getRootEndpoint() + "/profile";
    }

    @Override
    public DefaultOAuhtProfile extractUserProfile(final String body) {
        final DefaultOAuhtProfile profile = newProfile();
        
        // The parameter is obtained from the attributes
        final String attributesNode = "Get from attributes";
        JsonNode json = JsonHelper.getFirstNode(body);
        if(json ! =null) {
            profile.setId(ProfileHelper.sanitizeIdentifier(profile, JsonHelper.getElement(json, "id")));
            json = json.get(attributesNode);
            if(json ! =null) {
            
                // The return of CAS is treated differently
                if (json instanceof ArrayNode) {
                    final Iterator<JsonNode> nodes = json.iterator();
                    while (nodes.hasNext()) {
                        json = nodes.next();
                        finalString attribute = json.fieldNames().next(); convertAndAdd(profile, PROFILE_ATTRIBUTE, attribute, JsonHelper.getElement(json, attribute)); }}else if (json instanceof ObjectNode) {
                    final Iterator<String> keys = json.fieldNames();
                    while (keys.hasNext()) {
                        finalString key = keys.next(); convertAndAdd(profile, PROFILE_ATTRIBUTE, key, JsonHelper.getElement(json, key)); }}}else{ raiseProfileExtractionJsonError(body, attributesNode); }}else {
            raiseProfileExtractionJsonError(body);
        }
        returnprofile; }}Copy the code

DefaultDateConverter

This object is used to parse data, such as the time type here


public class DefaultDateConverter extends DateConverter {

    public DefaultDateConverter(a) {
        super("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
    }

    @Override
    public Date convert(final Object attribute) {
        Object a = attribute;
        if (a instanceof String) {
            String s = (String) a;
            int pos = s.lastIndexOf("[");
            if (pos > 0) {
                s = s.substring(0, pos);
                pos = s.lastIndexOf(":");
                if (pos > 0) {
                    s = s.substring(0, pos) + s.substring(pos + 1, s.length()); } a = s; }}return super.convert(a); }}Copy the code

DefaultOAuhtProfile

Can be understood as a TO TO receive data

public class DefaultOAuhtProfile extends OAuth20Profile {

    private static final long serialVersionUID = 1347249873352825528L;

    public Boolean isFromNewLogin(a) {
        return (Boolean) getAttribute(DefaultOAuthDefinition.IS_FROM_NEW_LOGIN);
    }

    public Date getAuthenticationDate(a) {
        return (Date) getAttribute(DefaultOAuthDefinition.AUTHENTICATION_DATE);
    }

    public String getAuthenticationMethod(a) {
        return (String) getAttribute(DefaultOAuthDefinition.AUTHENTICATION_METHOD);
    }

    public String getSuccessfulAuthenticationHandlers(a) {
        return (String) getAttribute(DefaultOAuthDefinition.SUCCESSFUL_AUTHENTICATION_HANDLERS);
    }

    public Boolean isLongTermAuthenticationRequestTokenUsed(a) {
        return(Boolean) getAttribute(DefaultOAuthDefinition.LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED); }}Copy the code

2.2 Build a receive object

    @GetMapping("callback")
    public void callBack(final HttpServletRequest request, final HttpServletResponse response) throws IOException {

        logger.info("------> [SSO callback pac4J OAuth logic] <-------");

        // Retrieve the cached object from Session
        OAuth20Client client = (OAuth20Client) request.getSession().getAttribute("client");
        J2EContext context = new J2EContext(request, response);

        // Obtain the Credentials corresponding to AccessToken
        final Credentials credentials = client.getCredentials(context);

        // Obtain a Profile from Profile
        final CommonProfile profile = client.getUserProfile(credentials, context);

        // The Web returns data information
        logger.info("------> Pac4j Demo obtain user information :[{}] <-------", profile.toString());
        response.getWriter().println(profile.toString());
    }

Copy the code

To sum it up:

  • DefaultOAuthAPI: Serves as metadata to identify the requested path
  • DefaultOAuthDefinition: Used by the interpreter to interpret the meaning of the return
  • DefaultDateConverter: Used to convert data
  • DefaultOAuhtProfile: To objects are used to host data

A simple customization that can be adapted to many different OAuth vendors

3. Source list

3.1 OAuth request

3.1.1 Authoriza process

The core class of Authoriza is IndirectClient. Let’s take a brief look at the logic of IndirectClient

The Authoriza authentication is initiated

C01- IndirectClient M101- redirect(WebContext context) ? As you can see earlier, we invoke redirect to initiate the request M102- getRedirectAction - if the request type is ajaxRequest, additional processing is done by ajaxRequestResolver -// M101 pseudocode
public final HttpAction redirect(WebContext context) {
    RedirectAction action = this.getRedirectAction(context);
    return action.perform(context);
}
    
// M102 pseudocode
public RedirectAction getRedirectAction(WebContext context) {
        this.init();
        if (this.ajaxRequestResolver.isAjax(context)) {
            RedirectAction action = this.redirectActionBuilder.redirect(context);
            this.cleanRequestedUrl(context);
            return this.ajaxRequestResolver.buildAjaxResponse(action.getLocation(), context);
        } else {
            String attemptedAuth = (String)context.getSessionStore().get(context, this.getName() + "$attemptedAuthentication");
            if (CommonHelper.isNotBlank(attemptedAuth)) {
                this.cleanAttemptedAuthentication(context);
                this.cleanRequestedUrl(context);
                throw HttpAction.unauthorized(context);
            } else {
                return this.redirectActionBuilder.redirect(context); }}} our fleet - RedirectActionBuilder (OAuth20RedirectActionBuilder) M201 - redirect - generate state and into the session2- this. The configuration. BuildService: build a OAuth20Service3- Obtain an authorizationUrl using the param attribute. -RedirectAction. redirect(authorizationUrl) : initiates authentication// M201 pseudocode
public RedirectAction redirect(WebContext context) {

    // This is pseudo code that generates state from generateState and puts it into session
    String state=this.configuration.isWithState()? generateState :null;

    // m201-2: OAuth20Service is the OAuth business class
    OAuth20Service service = (OAuth20Service)this.configuration.buildService(context, this.client, state);
    // m201-3: set the authentication address
    String authorizationUrl = service.getAuthorizationUrl(this.configuration.getCustomParams());
    return RedirectAction.redirect(authorizationUrl);

} 


// We haven't seen the actual request yet, so let's look at the bottomLet's go back to the M101 method perform c-redirectAction m-perform (WebContext context) -this.type == RedirectAction.RedirectType.REDIRECT ? HttpAction.redirect(context, this.location) : HttpAction.ok(context, this.content);
        
// The truth comes out
public static HttpAction redirect(WebContext context, String url) {
    context.setResponseHeader("Location", url);
    context.setResponseStatus(302);
    return new HttpAction(302); } He uses302The redirection status code, which is completed by the browser, where the charging relation address is HTTP:/ / 127.0.0.1 / sso/oauth2.0 / the authorize? The payload = code&client _id = b7a8cc2a a78 & redirect_uri - 5 dec - 4 = HTTP % % 2 f % 2 f127. 3 a 0.0.1%3 a9081%2 fmfa - client fcallba % 2 foauth % 2 ck%3Fclient_name%3DCasOAuthWrapperClient

Copy the code

Addendum 1: OAuth20Service

OAuth20Service is an OAuth business class that contains common OAuth operations

3.1.2 AccessToken process

In the previous article, we built a CallBack method for OAuth requests that will be called back after SSO authentication is complete. Let’s take a look at some of the interesting points:

    public void oauthCallBack(final HttpServletRequest request, final HttpServletResponse response) throws IOException {

        // This can be compared to building Context
        // This creates a loop that sends the state back
        CasOAuthWrapperClient client = (CasOAuthWrapperClient) request.getSession().getAttribute("oauthClient");
        
        // Step 2: Get the AccessToken
        J2EContext context = new J2EContext(request, response);
        final OAuth20Credentials credentials = client.getCredentials(context);
        
        final CommonProfile profile = client.getUserProfile(credentials, context);

        response.getWriter().println(profile.toString());


    }

Copy the code

So let’s see what the getCredentials method does

C01- IndirectClient
    M103- getCredentials(WebContext context)
        - thisInit () : request this is a main assertion - CommonHelper. AssertNotBlank ("key".this.key);
            - CommonHelper.assertNotBlank("secret".this.secret);
            - CommonHelper.assertNotNull("api".this.api);
            - CommonHelper.assertNotNull("hasBeenCancelledFactory".this.hasBeenCancelledFactory);
            - CommonHelper.assertNotNull("profileDefinition".this.profileDefinition);
    
    


    public final C getCredentials(WebContext context) {
        this.init();
        C credentials = this.retrieveCredentials(context);
        if (credentials == null) {
            context.getSessionStore().set(context, this.getName() + "$attemptedAuthentication"."true");
        } else {
            this.cleanAttemptedAuthentication(context);
        }

        return credentials;
    }
    
// Continue the index and see more complex
C03- BaseClient
    M301- retrieveCredentials
        -  this. CredentialsExtractor. Extract (context) : obtain a credentials object? - This object is the Code object previously returned by Authoriza after completion :PS001 -this.authenticator.validate(credentials, context) : Initiate validation? - This is OAuth20Authenticator, see more/ / added PS001
#OAuth20Credentials# | code: OC-1-wVu2cc3p33ChsQshKd1rUabk6lggPB1QhWh | accessToken: null| C04 - OAuth20Authenticator M401 - retrieveAccessToken - from OAuth20Credentials code - by OAuth20Configuration build OAuth20Service, call getAccessToken// M401 pseudo-code: this is very clear
    protected void retrieveAccessToken(WebContext context, OAuthCredentials credentials) {
        OAuth20Credentials oAuth20Credentials = (OAuth20Credentials)credentials;
        String code = oAuth20Credentials.getCode();
        this.logger.debug("code: {}", code);

        OAuth2AccessToken accessToken;
        try {
            accessToken = ((OAuth20Service)((OAuth20Configuration)this.configuration).buildService(context, this.client, (String)null)).getAccessToken(code);
        } catch (InterruptedException | ExecutionException | IOException var7) {
            throw new HttpCommunicationException("Error getting token:" + var7.getMessage());
        }

        this.logger.debug("accessToken: {}", accessToken);
        oAuth20Credentials.setAccessToken(accessToken);
    }
    
    
C05- OAuth20Service
    M501- getAccessToken
        - OAuthRequest request = this.createAccessTokenRequest(code, pkceCodeVerifier);
        - this.sendAccessTokenRequestSync(request);
    M502- sendAccessTokenRequestSync
        - (OAuth2AccessToken)this.api.getAccessTokenExtractor().extract(this.execute(request)); ? Httpclient.execute (userAgent, request.getheaders (), request.getVerb(), httpClient.execute(userAgent, request.getheaders (), request. request.getCompleteUrl(),request.getByteArrayPayload()); ? - PS002// PS002 Supplementary: See the following figure for parameters
http:/ / 127.0.0.1 / sso/oauth2.0 / accessToken?



        

Copy the code

3.1.3 the UserInfo

How to exchange Userinfo for AccessToken


// Step 1: The starting point
final CommonProfile profile = client.getUserProfile(credentials, context);

C03- BaseClient
    M302- getUserProfile
        -  U profile = retrieveUserProfile(credentials, context);
    M303- retrieveUserProfile
        - this.profileCreator.create(credentials, context); ? - OAuth20ProfileCreator: M601 C06- OAuthProfileCreator M601- create-t token =this.getAccesstoken (Credentials) : The Token - is obtainedreturn this.retrieveUserProfileFromToken(context, token);
    M602- retrieveUserProfileFromToken
        - finalOAuthProfileDefinition<U, T, O> profileDefinition = configuration.getProfileDefinition(); ? - OAuthProfileDefinition Builds the request, including the type to send -finalString profileUrl = profileDefinition.getProfileUrl(accessToken, configuration); ? - Profile Address -final S service = this.configuration.buildService(context, client, null); ? - Build a Service -finalString body = sendRequestForData(service, accessToken, profileUrl, profileDefinition.getProfileVerb()); ? - Request Profile, where the data is actually called -finalU profile = (U) configuration.getProfileDefinition().extractUserProfile(body); ? - Parse into a Profile object - addAccessTokenToProfile(Profile, accessToken); ? - Build the final objectCopy the code

3.2 the SAML article

3.2.1 Initiating a Request

// Step 1: Initiate a request- Build a Configuration - build a Client - because the SAML API is in metadata, So there is no need to inject API --> Invoke RedirectAction action = client.getreDirectAction (context); action.perform(context); -returnredirectActionBuilder.redirect(context); ? - the same routine, the builder here is SAML2RedirectActionBuilder// Finally build a SAML 302 request as usual

Copy the code

Look at the result of the request

3.2.2 Receiving data

The latter is still exactly the same, except that the Authenticator is now SAML2Authenticator


final SAML2Client client = (SAML2Client) request.getSession().getAttribute("samlclient");

// Get the J2EContext object
J2EContext context=new J2EContext(request,response);
final SAML2Credentials credentials = client.getCredentials(context);

// Obtain profile data
final CommonProfile profile = client.getUserProfile(credentials, context);
response.getWriter().println(profile.toString());

C- SAML2Authenticator
    M- validate
        - finalSAML2Profile profile = getProfileDefinition().newProfile(); ? - obtain the return Profile - Profile. AddAuthenticationAttribute (SESSION_INDEX, credentials. GetSessionIndex ()); - profile.addAuthenticationAttribute(SAML_NAME_ID_FORMAT, nameId.getFormat()); - profile.addAuthenticationAttribute(SAML_NAME_ID_NAME_QUALIFIER, nameId.getNameQualifier()); - profile.addAuthenticationAttribute(SAML_NAME_ID_SP_NAME_QUALIFIER, nameId.getSpNameQualifier()); - profile.addAuthenticationAttribute(SAML_NAME_ID_SP_PROVIDED_ID, nameId.getSpProviderId()); ? - Configure related properties// final CommonProfile profile = client.getUserProfile(credentials, context);

Copy the code

4. In-depth analysis

Pac4j is a good open source project. In terms of process, it has very good extensibility (PS: personal writing products like extensibility, everything can be matched), which is a big advantage in open source. Its overall process can be basically viewed as the following parts

The Configuration system

The Client system

Credentials system

Profile system

In the case of so many systems, the overall container coordination is done by Context, and the unified request redirection is done by RedirectAction.

Why specifically mention RedirectAction?

In my opinion, PAC4J abstracts all requests into two parts, one is to initiate authentication and the other is to return callback. With these two intuitive operations as the boundary, and then adding operations such as obtaining authentication information, the user basically does not see the call to the request.

SequenceDiagram Demo->>Demo ->>Demo ->>Demo ->>Demo ->>Demo client Demo->>SSO ->>Demo: Return authentication information Demo->>Demo: Definition parses and returns, obtaining a AccessToken Credentials Demo->>SSO: Obtains the authentication information through token SSO->>Demo: Return user information Demo->>Demo: resolves to Profile

5. Open source analysis

So what benefits can be learned from PAC4J?

First, what is the positioning of PAC4J?

Pac4j is an authentication tool, or SDK, that solves the complexity of the authentication process and allows users to access user information directly with a simple call.

The first advantage is compatibility and configurability. I provide so many clients that you can adjust them directly or customize them yourself. It doesn’t matter.


The second advantage of PAC4J in terms of code structure is clarity.

We can feel from the above analysis, what can be done, how to do, how to name, in fact, the specification is good, simple implementation can meet the requirements.


And the third advantage, I think, is low coupling.

The way THAT Pac4J aggregates Maven, you can just reference the relevant package to implement the protocol. There is also a low code-to-code dependency, which is also a great benefit for customization and worth learning from.

6. Summary

The pac4J tool is an ideal tool if you want to get it online quickly,

Personal when writing demo, also often use him to do basic test, don’t say, really good use

The code has been posted on Git (case 4.6.2) and can be viewed directly.