I am trying to extend keycloak by creating a new endpoint to authenticate users.
The point is, user is not stored in keycloak, the user is stored in an external system.
The external system will call the new endpoint and provide token (will contains user info), clientId, and clientSecret. and (somehow) we will verify the existence of the user.
The challenge I am facing right now is I cannot create a session for the user. (seems the session requires existed user in keycloak, I am using InMemoryUser)
package com.mhewedy.auth; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.Config; import org.keycloak.events.EventBuilder; import org.keycloak.models.*; import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProviderFactory; import org.keycloak.services.resources.Cors; import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.storage.adapter.InMemoryUserAdapter; import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.UUID; import static org.keycloak.services.resources.Cors.ACCESS_CONTROL_ALLOW_ORIGIN; import static org.keycloak.utils.MediaType.APPLICATION_JSON_TYPE; public class MyEndpoint extends AuthorizationEndpointBase implements RealmResourceProvider { private final Logger logger = Logger.getLogger(MyEndpoint.class); private final TokenManager tokenManager = new TokenManager(); public MyEndpoint(RealmModel realm, EventBuilder event) { super(realm, event); } @Override public Object getResource() { return this; } @Override public void close() { } @GET @Path("authenticate") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response authenticate(@FormParam("token") String token, @FormParam("client_id") String clientId, @FormParam("client_secret") String clientSecret, @Context HttpRequest request) { // validate client_id & client_secret // validate token logger.info("generating access token..."); String userId = UUID.randomUUID().toString(); UserModel userModel = new InMemoryUserAdapter(session, session.getContext().getRealm(), userId); userModel.setUsername(token); userModel.setEnabled(true); // this session object doesn't contain the userModel, cause it seems it lookups the user by id and doesn't find it UserSessionModel userSession = session.sessions().createOfflineUserSession(session.sessions().createUserSession( session.getContext().getRealm(), userModel, token, "192.168.1.1", "My", false, null, null )); ClientModel clientModel = realm.getClientByClientId(clientId); logger.infof("Configurable token requested for username=%s and client=%s on realm=%s", userModel.getUsername(), clientModel.getClientId(), realm.getName()); AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, clientModel, userSession); ClientSessionContext clientSessionContext = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, session); AccessToken newToken = tokenManager .createClientAccessToken(session, realm, clientModel, userModel, userSession, clientSessionContext); newToken.expiration(10 * 10 * 10 * 10); EventBuilder eventBuilder = new EventBuilder(realm, session, session.getContext().getConnection()); AccessTokenResponse response = tokenManager .responseBuilder(realm, clientModel, eventBuilder, session, userSession, clientSessionContext) .accessToken(newToken) .build(); return buildCorsResponse(request, response); } private Response buildCorsResponse(@Context HttpRequest request, AccessTokenResponse response) { Cors cors = Cors.add(request) .auth() .allowedMethods("POST") .auth() .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN) .allowAllOrigins(); return cors.builder(Response.ok(response).type(APPLICATION_JSON_TYPE)).build(); } // ---------------------------------------------------------------------------------------------------------------- public static class MyEndpointFactory implements RealmResourceProviderFactory { @Override public RealmResourceProvider create(KeycloakSession session) { KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); EventBuilder event = new EventBuilder(realm, session, context.getConnection()); MyEndpoint provider = new MyEndpoint(realm, event); ResteasyProviderFactory.getInstance().injectProperties(provider); return provider; } @Override public void init(Config.Scope config) { } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public void close() { } @Override public String getId() { return "MyEndpoint"; } } }
I am using code from here but the use case is differnt.
Advertisement
Answer
I solved by saving the user in the cache (db) if not exist:
String username = getUsernameFromToken(token); String userId = "my-" + username; UserModel userModel = new InMemoryUserAdapter(session, session.getContext().getRealm(), username); userModel.setUsername(username); userModel.setEnabled(true); if (session.users().getUserById(realm, userId) == null) { session.userCache().addUser(realm, userId, username, false, false); }