Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

Guide To The Java Authentication And Authorization Service (JAAS)

$
0
0

1. Overview

Java Authentication And Authorization Service (JAAS) is a Java SE low-level security framework that augments the security model from code-based security to user-based security. We can use JAAS for two purposes:

  • Authentication: Identifying the entity that is currently running the code
  • Authorization: Once authenticated, ensure that this entity has the required access control rights or permissions to execute sensitive code

In this tutorial, we'll cover how to set up JAAS in a sample application by implementing and configuring its various APIs, especially the LoginModule.

2. How JAAS Works

When using JAAS in an application, several APIs are involved:

  • CallbackHandler: Used for gathering user credentials and optionally provided when creating the LoginContext
  • Configuration: Responsible for loading LoginModule implementations and can be optionally provided at LoginContext creation
  • LoginModule: Effectively used for authenticating users

We'll use the default implementation for the Configuration API and provide our own implementations for the CallbackHandler and the LoginModule APIs.

3. Providing CallbackHandler Implementation

Before digging into the LoginModule implementation, we first need to provide an implementation for the CallbackHandler interface, which is used for gathering user credentials.

It has a single method, handle(), that accepts an array of Callbacks. In addition, JAAS already provides many Callback implementations, and we'll be using the NameCallback and PasswordCallback for gathering the username and password, respectively.

Let's see our implementation of the CallbackHandler interface:

public class ConsoleCallbackHandler implements CallbackHandler {

    @Override
    public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
        Console console = System.console();
        for (Callback callback : callbacks) {
            if (callback instanceof NameCallback) {
                NameCallback nameCallback = (NameCallback) callback;
                nameCallback.setName(console.readLine(nameCallback.getPrompt()));
            } else if (callback instanceof PasswordCallback) {
                PasswordCallback passwordCallback = (PasswordCallback) callback;
                passwordCallback.setPassword(console.readPassword(passwordCallback.getPrompt()));
            } else {
                throw new UnsupportedCallbackException(callback);
            }
        }
    }
}

So, to prompt and read the username, we've used:

NameCallback nameCallback = (NameCallback) callback;
nameCallback.setName(console.readLine(nameCallback.getPrompt()));

Similarly, to prompt and read the password:

PasswordCallback passwordCallback = (PasswordCallback) callback;
passwordCallback.setPassword(console.readPassword(passwordCallback.getPrompt()));

Later, we'll see how to call the CallbackHandler when implementing the LoginModule.

4. Providing LoginModule Implementation

For simplicity, we'll provide an implementation that stores hard-coded users. So, let's call it InMemoryLoginModule:

public class InMemoryLoginModule implements LoginModule {

    private static final String USERNAME = "testuser";
    private static final String PASSWORD = "testpassword";

    private Subject subject;
    private CallbackHandler callbackHandler;
    private Map<String, ?> sharedState;
    private Map<String, ?> options;
    
    private boolean loginSucceeded = false;
    private Principal userPrincipal;
    //...
}

In the next subsections, we'll be giving an implementation for the more important methods: initialize(), login(), and commit().

4.1. initialize()

The LoginModule is first loaded and then initialized with a Subject and a CallbackHandler. Additionally, LoginModules can use a Map for sharing data among themselves, and another Map for storing private configuration data:

public void initialize(
  Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
    this.subject = subject;
    this.callbackHandler = callbackHandler;
    this.sharedState = sharedState;
    this.options = options;
}

4.2. login()

In the login() method, we invoke the CallbackHandler.handle() method with a NameCallback and a PasswordCallback to prompt and get the username and password. Then, we compare these provided credentials with the hardcoded ones:

@Override
public boolean login() throws LoginException {
    NameCallback nameCallback = new NameCallback("username: ");
    PasswordCallback passwordCallback = new PasswordCallback("password: ", false);
    try {
        callbackHandler.handle(new Callback[]{nameCallback, passwordCallback});
        String username = nameCallback.getName();
        String password = new String(passwordCallback.getPassword());
        if (USERNAME.equals(username) && PASSWORD.equals(password)) {
            loginSucceeded = true;
        }
    } catch (IOException | UnsupportedCallbackException e) {
        //...
    }
    return loginSucceeded;
}

The login() method should return true for a successful operation and false for a failed login.

4.3. commit()

If all calls to LoginModule#login succeed, we update the Subject with an additional Principal:

@Override
public boolean commit() throws LoginException {
    if (!loginSucceeded) {
        return false;
    }
    userPrincipal = new UserPrincipal(username);
    subject.getPrincipals().add(userPrincipal);
    return true;
}

Otherwise, the abort() method is called.

At this point, our LoginModule implementation is ready and needs to be configured so that it can be loaded dynamically using the Configuration service provider.

5. LoginModule Configuration

JAAS uses the Configuration service provider to load LoginModules at runtime. By default, it provides and uses the ConfigFile implementation where LoginModules are configured through a login file. For example, here is the content of the file used for our LoginModule:

jaasApplication {
   com.baeldung.jaas.loginmodule.InMemoryLoginModule required debug=true;
};

As we can see, we've provided the fully qualified class name of the LoginModule implementation, a required flag, and an option for debugging.

Finally, note that we can also specify the login file through the java.security.auth.login.config system property:

$ java -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config

We can also specify one or more login files through the property login.config.url in the Java security file, ${java.home}/jre/lib/security/java.security:

login.config.url.1=file:${user.home}/.java.login.config

6. Authentication

Firstly, an application initializes the authentication process by creating a LoginContext instance. To do so, we can take a look at the full constructor to have an idea about what we need as parameters:

LoginContext(String name, Subject subject, CallbackHandler callbackHandler, Configuration config)
  • name: used as an index for loading only the corresponding LoginModules
  • subject: represents a user or service that wants to log in
  • callbackHandler: responsible for passing user credentials from the application to the LoginModule
  • config: responsible for loading LoginModules that correspond to the name parameter

Here, we'll be using the overloaded constructor where we'll be providing our CallbackHandler implementation:

LoginContext(String name, CallbackHandler callbackHandler)

Now that we have a CallbackHandler and a configured LoginModule, we can start the authentication process by initializing a LoginContext object:

LoginContext loginContext = new LoginContext("jaasApplication", new ConsoleCallbackHandler());

At this point, we can invoke the login() method to authenticate the user:

loginContext.login();

The login() method, in turn, creates a new instance of our LoginModule and calls its login() method. And, upon successful authentication, we can retrieve the authenticated Subject:

Subject subject = loginContext.getSubject();

Now, let's run a sample application that has the LoginModule wired in:

$ mvn clean package
$ java -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config \
    -classpath target/core-java-security-2-0.1.0-SNAPSHOT.jar com.baeldung.jaas.JaasAuthentication

When we're prompted to provide the username and password, we'll use testuser and testpassword as credentials.

7. Authorization

Authorization comes into play when the user is first connected and associated with the AccessControlContext. Using the Java security policy, we can grant one or more access control rights to Principals. We can then prevent access to sensitive code by calling the SecurityManager#checkPermission method:

SecurityManager.checkPermission(Permission perm)

7.1. Defining Permissions

An access control right or permission is the ability to execute an action on a resource. We can implement a permission by subclassing the Permission abstract class. To do so, we need to provide a resource name and a set of possible actions. For example, we can use FilePermission to configure access control rights on files. Possible actions are read, write, execute, and so on. For scenarios where actions are not necessary, we may simply use the BasicPermision.

Next, we'll provide an implementation of permission through the ResourcePermission class where users may have permission to access a resource:

public final class ResourcePermission extends BasicPermission {
    public ResourcePermission(String name) {
        super(name);
    }
}

Later, we'll configure an entry for this permission through the Java security policy.

7.2. Granting Permissions

Usually, we don't need to know the policy file syntax because we can always use the Policy Tool to create one. Let's take a look at our policy file:

grant principal com.sun.security.auth.UserPrincipal testuser {
    permission com.baeldung.jaas.ResourcePermission "test_resource"
};

In this sample, we've granted the test_resource permission to the testuser user.

7.3. Checking Permissions

Once the Subject is authenticated and permissions are configured, we can check for access by calling the Subject#doAs or Subject#doAsPrivilieged static methods. For this purpose, we'll provide a PrivilegedAction where we can protect access to sensitive code. In the run() method, we call the SecurityManager#checkPermission method to ensure that the authenticated user has the test_resource permission:

public class ResourceAction implements PrivilegedAction {
    @Override
    public Object run() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new ResourcePermission("test_resource"));
        }
        System.out.println("I have access to test_resource !");
        return null;
    }
}

The last thing is to call the Subject#doAsPrivileged method:

Subject subject = loginContext.getSubject();
PrivilegedAction privilegedAction = new ResourceAction();
Subject.doAsPrivileged(subject, privilegedAction, null);

Like the authentication, we'll run a simple application for the authorization where, in addition to the LoginModule, we provide a permissions configuration file:

$ mvn clean package
$ java -Djava.security.manager -Djava.security.policy=src/main/resources/jaas/jaas.policy \
    -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config \
    -classpath target/core-java-security-2-0.1.0-SNAPSHOT.jar com.baeldung.jaas.JaasAuthorization

8. Conclusion

In this article, we have showcased how to implement JAAS by exploring the principal classes and interfaces and showing how to configure them. Especially, we have implemented a service provider LoginModule.

As usual, the code in this article is available over on GitHub.


Viewing all articles
Browse latest Browse all 4535

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>