Skip to content

Commit

Permalink
feat: add authentication (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxime1907 authored Dec 13, 2022
1 parent 295726a commit 1f376df
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 33 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ Request an action token for a set of specific actions.
- https://stackoverflow.com/a/67636834
- https://github.com/dteleguin/beercloak/blob/master/beercloak-module/src/main/java/beercloak/providers/BeerResourceProvider.java
- https://github.com/dteleguin/beercloak/blob/master/beercloak-module/src/main/java/beercloak/resources/AbstractAdminResource.java
- https://github.com/keycloak/keycloak/blob/1ed81fa3772220cb8018654e40645f981c934da6/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java#L156
- https://github.com/keycloak/keycloak/blob/264c5a6cdb2fb86e20536ea4302d20160ca01919/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java#L284
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<artifactId>actions-token</artifactId>
<groupId>org.jboss.aerogear</groupId>
<packaging>jar</packaging>
<version>0.0.5</version>
<version>0.0.6</version>

<properties>
<java.version>11</java.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
Expand All @@ -19,13 +23,24 @@
import org.keycloak.TokenCategory;
import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.AdminAuth;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;

import com.google.gson.Gson;

Expand All @@ -36,6 +51,8 @@ public class ActionsTokenResource {

private final KeycloakSession session;

private AdminPermissionEvaluator realmAuth;

public ActionsTokenResource(KeycloakSession session) {
this.session = session;
}
Expand All @@ -48,9 +65,23 @@ public ActionsTokenResource(KeycloakSession session) {
public Response getActionToken(
String jsonString,
@Context UriInfo uriInfo) {
// if (this.auth == null || this.auth.getToken() == null) {
// throw new NotAuthorizedException("Bearer");
// }
KeycloakContext context = session.getContext();

RealmModel realm = session.getContext().getRealm();

AdminAuth auth = authenticateRealmAdminRequest(context.getRequestHeaders());

RealmManager realmManager = new RealmManager(session);
if (realm == null) throw new NotFoundException("Realm not found.");

if (!auth.getRealm().equals(realmManager.getKeycloakAdminstrationRealm())
&& !auth.getRealm().equals(realm)) {
throw new org.keycloak.services.ForbiddenException();
}

realmAuth = AdminPermissions.evaluator(session, realm, auth);

session.getContext().setRealm(realm);

ActionTokenRequest actionTokenRequest = null;
try {
Expand All @@ -61,14 +92,40 @@ public Response getActionToken(
ErrorResponse.error("Invalid json input.", Status.BAD_REQUEST));
}

log.debugf("%s", actionTokenRequest.userId);
log.debugf("%s", actionTokenRequest.redirectUri);
log.debugf("%s", actionTokenRequest.clientId);
log.debugf("%s", actionTokenRequest.actions);
log.debugf("%s", actionTokenRequest.redirectUriValidate);
log.debugf("%s", actionTokenRequest.lifespan);
UserModel user = session.users().getUserById(realm, actionTokenRequest.userId);
if (user == null) {
// we do this to make sure somebody can't phish ids
if (realmAuth.users().canQuery())
throw new NotFoundException("User not found");
else
throw new ForbiddenException();
}

KeycloakContext context = session.getContext();
// Can parameterize this as well
List<String> requiredActions = new LinkedList<String>();

try {
for (int i = 0; i < actionTokenRequest.actions.size(); i++) {
String requiredActionName = actionTokenRequest.actions.get(i);
RequiredAction requiredAction = RequiredAction.valueOf(requiredActionName);
requiredActions.add(requiredAction.name());
}
} catch (IllegalArgumentException cause) {
throw new WebApplicationException(
ErrorResponse.error("Invalid requiredAction.", Status.BAD_REQUEST));
}

realmAuth.users().requireManage(user);

if (requiredActions.contains(RequiredAction.VERIFY_EMAIL.name()) && user.getEmail() == null)
{
return ErrorResponse.error("User email missing", Status.BAD_REQUEST);
}

if (!user.isEnabled()) {
throw new WebApplicationException(
ErrorResponse.error("User is disabled", Status.BAD_REQUEST));
}

if (actionTokenRequest.redirectUri != null && actionTokenRequest.clientId == null) {
throw new WebApplicationException(
Expand All @@ -79,7 +136,7 @@ public Response getActionToken(
actionTokenRequest.clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
}

ClientModel client = assertValidClient(actionTokenRequest.clientId, context);
ClientModel client = assertValidClient(actionTokenRequest.clientId);
if (actionTokenRequest.redirectUri != null && actionTokenRequest.redirectUriValidate != null && actionTokenRequest.redirectUriValidate)
assertValidRedirectUri(actionTokenRequest.redirectUri, client);

Expand All @@ -89,20 +146,6 @@ public Response getActionToken(
validityInSecs = actionTokenRequest.lifespan;
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;

// Can parameterize this as well
List<String> requiredActions = new LinkedList<String>();

try {
for (int i = 0; i < actionTokenRequest.actions.size(); i++) {
String requiredActionName = actionTokenRequest.actions.get(i);
RequiredAction requiredAction = RequiredAction.valueOf(requiredActionName);
requiredActions.add(requiredAction.name());
}
} catch (IllegalArgumentException cause) {
throw new WebApplicationException(
ErrorResponse.error("Invalid requiredAction.", Status.BAD_REQUEST));
}

ExecuteActionsActionToken token = new ExecuteActionsActionToken(
actionTokenRequest.userId,
absoluteExpirationInSecs,
Expand Down Expand Up @@ -136,8 +179,8 @@ private void assertValidRedirectUri(String redirectUri, ClientModel client) {
}
}

private ClientModel assertValidClient(String clientId, KeycloakContext context) {
ClientModel client = context.getRealm().getClientByClientId(clientId);
private ClientModel assertValidClient(String clientId) {
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
if (client == null) {
log.debugf("Client %s doesn't exist", clientId);
throw new WebApplicationException(
Expand All @@ -150,4 +193,42 @@ private ClientModel assertValidClient(String clientId, KeycloakContext context)
}
return client;
}
}

protected AdminAuth authenticateRealmAdminRequest(HttpHeaders headers) {
String tokenString = AppAuthManager.extractAuthorizationHeaderToken(headers);
if (tokenString == null) throw new NotAuthorizedException("Bearer");
AccessToken token;
try {
JWSInput input = new JWSInput(tokenString);
token = input.readJsonContent(AccessToken.class);
} catch (JWSInputException e) {
throw new NotAuthorizedException("Bearer token format error");
}
String realmName = token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1);
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName);
if (realm == null) {
throw new NotAuthorizedException("Unknown realm in token");
}
session.getContext().setRealm(realm);

AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)
.setRealm(realm)
.setConnection(session.getContext().getConnection())
.setHeaders(headers)
.authenticate();

if (authResult == null) {
log.debug("Token not valid");
throw new NotAuthorizedException("Bearer");
}

ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (client == null) {
throw new NotFoundException("Could not find client for authorization");

}

return new AdminAuth(realm, authResult.getToken(), authResult.getUser(), client);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.github.maxime1907.keycloak.actions.token;

import org.keycloak.models.KeycloakSession;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
import org.keycloak.services.resource.RealmResourceProvider;


Expand All @@ -12,11 +10,9 @@ public class ActionsTokenResourceProvider implements RealmResourceProvider {
public final static String ID = "actions-token";

private KeycloakSession session;
private AuthResult auth;

public ActionsTokenResourceProvider(KeycloakSession session) {
this.session = session;
this.auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
}

@Override
Expand Down

0 comments on commit 1f376df

Please sign in to comment.