tomaytotomato
tomaytotomato

Reputation: 4028

Creating JSON Web Tokens through Basic Authentication endpoint? Dropwizard

Using Dropwizard 1.2.0 and Dropwizard JWT library, I am trying to create json web tokens from an API endpoint called /token

This endpoint requires the client to pass a username and password, using Basic Authentication method. If successful the response will contain a JSON web token.

Principal

public class ShepherdAuth implements JwtCookiePrincipal {

private String name;
private Set<String> roles;

public ShepherdAuth(String name, Set<String> roles) {
    this.name = checkNotNull(name, "User name is required");
    this.roles = checkNotNull(roles, "Roles are required");
}

@Override
public boolean isPersistent() {
    return false;
}

@Override
public boolean isInRole(final String s) {
    return false;
}

@Override
public String getName() {
    return this.name;
}

@Override
public boolean implies(Subject subject) {
    return false;
}

public Set<String> getRoles() {
    return roles;
}
}

Authenticator

public class ShepherdAuthenticator implements Authenticator<BasicCredentials, ShepherdAuth> {

private static final Map<String, Set<String>> VALID_USERS = ImmutableMap.of(
        "guest", ImmutableSet.of(),
        "shepherd", ImmutableSet.of("SHEPHERD"),
        "admin", ImmutableSet.of("ADMIN", "SHEPHERD")
);

@Override
public Optional<ShepherdAuth> authenticate(BasicCredentials credentials) throws AuthenticationException {
    if (VALID_USERS.containsKey(credentials.getUsername()) && "password".equals(credentials.getPassword())) {
        return Optional.of(new ShepherdAuth(credentials.getUsername(), VALID_USERS.get(credentials.getUsername())));
    }
    return Optional.empty();
}
}

Resource / Controller

public class ShepherdController implements ShepherdApi {

public ShepherdController() {
}

@PermitAll
@GET
@Path("/token")
public ShepherdAuth auth(@Auth final BasicCredentials user) {
    return new ShepherdAuth(user.getUsername(),
        ImmutableSet.of("SHEPHERD"));
}

App / Config

    @Override
public void run(final ShepherdServiceConfiguration configuration,
                final Environment environment) {

    final ShepherdController shepherdController = new ShepherdController();

    // app authentication
    environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<ShepherdAuth>()
            .setAuthenticator(new ShepherdAuthenticator())
            .setAuthorizer(new ShepherdAuthorizer())
            .setRealm(configuration.getName())
            .buildAuthFilter()));

When I try to make a request to /shepherd/token I do not get a prompt for basic auth, instead I get a HTTP 401 response with

Credentials are required to access this resource.

How do I get the controller to prompt for username and password and generate a JWT on success?

Upvotes: 1

Views: 1039

Answers (2)

David
David

Reputation: 1074

Your missing this line in your configuration.

environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));

Upvotes: 1

Can Bican
Can Bican

Reputation: 464

I implemented JWT tokens in my project by using https://github.com/jwtk/jjwt but the solution will easily be applied to another library. The trick is to use different authenticators.

This answer is not suited for Dropwizard JWT Library but does the fine job of providing JWT for Dropwizard :)

First, the application:

environment.jersey().register(new TokenResource(configuration.getJwsSecretKey()));
environment.jersey().register(new HelloResource());
environment.jersey().register(RolesAllowedDynamicFeature.class);
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
environment.jersey()
    .register(
        new AuthDynamicFeature(
            new ChainedAuthFilter<>(
                Arrays
                    .asList(
                        new JWTCredentialAuthFilter.Builder<User>()
                            .setAuthenticator(
                                new JWTAuthenticator(configuration.getJwsSecretKey()))
                            .setPrefix("Bearer").setAuthorizer(new UserAuthorizer())
                            .buildAuthFilter(),
                        new JWTDefaultCredentialAuthFilter.Builder<User>()
                            .setAuthenticator(new JWTDefaultAuthenticator())
                            .setAuthorizer(new UserAuthorizer()).setRealm("SUPER SECRET STUFF")
                            .buildAuthFilter()))));

Please note that the configuration class must contain a configuration setting:

String jwsSecretKey;

Here, the TokenResource is the token supplying resource, and the HelloResource is our test resource. User is the principal, like this:

public class User implements Principal {
  private String name;
  private String password;
  ...
}

And there is one class for communicating the JWT token:

public class JWTCredentials {
  private String jwtToken;
  ...
}

TokenResource provides tokens for a user "test" with password "test":

@POST
@Path("{user}")
@PermitAll
public String createToken(@PathParam("user") String user, String password) {
  if ("test".equals(user) && "test".equals(password)) {
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    long nowMillis = System.currentTimeMillis();
    Date now = new Date(nowMillis);
    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(this.secretKey);
    Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
    JwtBuilder builder = Jwts.builder().setIssuedAt(now).setSubject("test")
      .signWith(signatureAlgorithm, signingKey);
    return builder.compact();
  }
  throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}

And the HelloResource just echoes back the user:

@GET
@RolesAllowed({"ANY"})
public String hello(@Auth User user) {
  return "hello user \"" + user.getName() + "\"";
}

JWTCredentialAuthFilter provides credentials for both authentication schemes:

@Priority(Priorities.AUTHENTICATION)
public class JWTCredentialAuthFilter<P extends Principal> extends AuthFilter<JWTCredentials, P> {

  public static class Builder<P extends Principal>
      extends AuthFilterBuilder<JWTCredentials, P, JWTCredentialAuthFilter<P>> {

    @Override
    protected JWTCredentialAuthFilter<P> newInstance() {
      return new JWTCredentialAuthFilter<>();
    }
  }

  @Override
  public void filter(ContainerRequestContext requestContext) throws IOException {
    final JWTCredentials credentials =
    getCredentials(requestContext.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
    if (!authenticate(requestContext, credentials, "JWT")) {
      throw new WebApplicationException(
          this.unauthorizedHandler.buildResponse(this.prefix, this.realm));
    }
  }

  private static JWTCredentials getCredentials(String authLine) {
    if (authLine != null && authLine.startsWith("Bearer ")) {
      JWTCredentials result = new JWTCredentials();
      result.setJwtToken(authLine.substring(7));
      return result;
    }
    return null;
  }
}

JWTAuthenticator is when JWT credentials are provided:

public class JWTAuthenticator implements Authenticator<JWTCredentials, User> {

  private String secret;

  public JWTAuthenticator(String jwtsecret) {
    this.secret = jwtsecret;
  }

  @Override
  public Optional<User> authenticate(JWTCredentials credentials) throws AuthenticationException {
    try {
      Claims claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(this.secret))
      .parseClaimsJws(credentials.getJwtToken()).getBody();
      User user = new User();
      user.setName(claims.getSubject());
      return Optional.ofNullable(user);
    } catch (@SuppressWarnings("unused") ExpiredJwtException | UnsupportedJwtException
        | MalformedJwtException | SignatureException | IllegalArgumentException e) {
      return Optional.empty();
    }
  }
}

JWTDefaultAuthenticator is when no credentials are present, giving the code an empty user:

public class JWTDefaultAuthenticator implements Authenticator<JWTCredentials, User> {

  @Override
  public Optional<User> authenticate(JWTCredentials credentials) throws AuthenticationException {
    return Optional.of(new User());
  }
}

UserAuthorizer permits the "ANY" role, as long as the user is not null:

public class UserAuthorizer implements Authorizer<User> {
  @Override
  public boolean authorize(User user, String role) {
    return user != null && "ANY".equals(role)
  }
}

If all goes well,

curl -s -X POST -d 'test' http://localhost:8080/token/test

will give you something like:

eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MDk3MDYwMjYsInN1YiI6InRlc3QifQ.ZrRmWTUDpaA6JlU4ysIcFllxtqvUS2OPbCMJgyou_tY

and this query

curl -s -X POST -d 'xtest' http://localhost:8080/token/test

will fail with

{"code":401,"message":"HTTP 401 Unauthorized"}

(BTW, "test" in the URL is the user name and "test" in the post data is the password. As easy as basic auth, and can be configured for CORS.)

and the request

curl -s -X GET -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MDk3MDYwMjYsInN1YiI6InRlc3QifQ.ZrRmWTUDpaA6JlU4ysIcFllxtqvUS2OPbCMJgyou_tY' http://localhost:8080/hello

will show

hello user "test"

while

curl -s -X GET -H 'Authorization: Bearer invalid' http://localhost:8080/hello

and

curl -s -X GET http://localhost:8080/hello

will result in

{"code":403,"message":"User not authorized."}

Upvotes: 2

Related Questions