Logging a user in directly from an ActionToken

Hi,

I’m working on a requirement to have an ActionToken directly log in a user (sort of like “magic link”). I had a couple of thoughts that seem hack-y, and I wanted to share here in the hope I’d get some feedback.

  • In the ActionTokenHandler, do direct naked impersonation / token exchange for the user specified in the action token. Set the resulting ID token as the KEYCLOAK_IDENTITY cookie and then redirect to a normal auth session. The CookieAuthenticator would see the KEYCLOAK_IDENTITY cookie and complete a successful auth session

  • In the ActionTokenHandler, set a cookie BLAH_COOKIE with the action token itself. Redirect to a normal auth session. Build an Authenticator that you put first in the auth flow that reads BLAH_COOKIE validates the token, and then set the user and return success.

Both of them seem a bit dangerous, but the risk is mitigated by the fact that the (short lifespan) ActionToken is being sent to their email, which is all that is required for reset password.

Any ideas or feedback? Am I missing an obvious way to do this? Are these stupid ways to go about it?

Thanks!

I did something similar for a customer. Yes, it‘s kind of tricky and hacky, and maybe stupid and dangerous. But it‘s a way of solving some of the customers needs. At least in my case I discussed it entirely with my customer.

I didn‘t set any cookies, instead I implemented a custom authenticator which reads the custom action token and sets the user info in the auth session context and also set a redirect url, which is also included as a claim in the action token.
I‘ll have to look if I can extract some of the code or collect some more detailed info about what I did, but it‘s by contract IP of the customer…

1 Like

Thanks for the feedback! It’s very useful as a gut-check that I’m not missing anything. I have also discussed it with the customer, and they’re aware that there may be heightened risk.

So, I just had a chance to review my implementation.

I have a CustomActionToken (extends DefaultActionToken) and a corresponding CustomActionTokenHandler (extending AbstractActionTokenHandler<CustomActionToken>). The handler class does all the in my use case necessary logic and because it’s an action token, the user is identified automatically from the token.
In the handleToken() method, I set

authSession.setAuthNote("configuredFor", CustomActionToken.TOKEN_TYPE);

Additionally, there is a CustomActionTokenAuthenticator, these are the important parts:

@Override
public void authenticate(AuthenticationFlowContext context) {
  context.success();
}
@Override
public boolean requiresUser() {
  return true;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
  return CustomActionToken.TOKEN_TYPE.equals(session.getContext().getAuthenticationSession().getAuthNote("configuredFor"));
}

So, basically, the token handler sets an auth session note, that the auth session of this user is using the custom action token to login, this evaluates to true in the token authenticator configuredFor() method and authentication context is set to success.

WDYT about this?
Do you think there’s a security issue with this?

Thank you for sharing your implementation. Assuming you’re validating the action token in your handler, I can’t see any reason not to just allow it based on the auth note in your Authenticator. I am trying a very similar thing in the Authenticator. I pass the action token through as an auth note, and then double-check its validity in the configuredFor method (probably unnecessary to do it twice).

My ActionTokenHandler is where I’m having problems setting up the session properly:

  @Override
  public Response handleToken(
      PortalLinkActionToken token,
      ActionTokenContext<PortalLinkActionToken> tokenContext) {
    UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
    AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    ClientModel client = authSession.getClient();

    String redirectUri =
        token.getRedirectUri() != null
            ? token.getRedirectUri()
            : ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), client.getBaseUrl());
    log.infof("Using redirect_uri %s", redirectUri);
    String redirect = RedirectUtils.verifyRedirectUri(tokenContext.getSession(), redirectUri, authSession.getClient());
    if (redirect != null) {
      authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");
      authSession.setRedirectUri(redirect);
      authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
    }

    String tokenString = token.serialize(tokenContext.getSession(), tokenContext.getRealm(), tokenContext.getUriInfo());
    authSession.setAuthNote(ORIGINAL_ACTION_TOKEN, tokenString);

    return tokenContext.processFlow(true, AUTHENTICATE_PATH, tokenContext.getRealm().getBrowserFlow(), null, new AuthenticationProcessor());
  }

However, this doesn’t work, as the authentication flow is expired. Do I need to create a new authentication flow (e.g. with tokenContext.createAuthenticationSessionForClient(clientId)) and then add the session from the handler as a compound session? Let me know if you see anything obvious about session setup.

Hm, don’t see anything obvious.
But I have also this in my token handler:

@Override
public AuthenticationSessionModel startFreshAuthenticationSession(CustomActionToken token, ActionTokenContext<CustomActionToken> tokenContext) {
  return tokenContext.createAuthenticationSessionForClient(token.getIssuedFor());
}

The default impl in the abstract class sets an additonal auth note to end the auth session (at least this is my understanding)

1 Like

Thanks for that tip. It looks like the AbstractActionTokenHandler sets a note that the session should be terminated after required actions are executed:

I took your suggestion and did override that method. The thought that I would need to include required actions explicitly sent me to a different approach in my ActionTokenHandler. I settled on this, which works with no custom Authenticator required (it skips the authentication flow entirely):

  @Override
  public AuthenticationSessionModel startFreshAuthenticationSession(PortalLinkActionToken token, ActionTokenContext<PortalLinkActionToken> tokenContext) {
    return tokenContext.createAuthenticationSessionForClient(token.getIssuedFor());
  }
  
  @Override
  public Response handleToken(
      PortalLinkActionToken token,
      ActionTokenContext<PortalLinkActionToken> tokenContext) {
    UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
    AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    ClientModel client = authSession.getClient();

    String redirectUri =
        token.getRedirectUri() != null
            ? token.getRedirectUri()
            : ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), client.getBaseUrl());
    log.infof("Using redirect_uri %s", redirectUri);
    String redirect = RedirectUtils.verifyRedirectUri(tokenContext.getSession(), redirectUri, authSession.getClient());
    if (redirect != null) {
      authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");
      authSession.setRedirectUri(redirect);
      authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
    }

    String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getRequest(), tokenContext.getEvent());
    return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction);

    // This doesn't work. Why?
    //return tokenContext.processFlow(true, AUTHENTICATE_PATH, tokenContext.getRealm().getBrowserFlow(), null, new AuthenticationProcessor());
  }

Although, I still don’t really understand why the processFlow doesn’t forward to the normal browser flow. In any case, thanks again for all the help, and I hope we can continue the discussion here if someone finds or can contribute better information.

Another extension where I used this approach: Magic Link login: Authenticator and Resource

Hi!

Thanks for your samples. They are really helpful!

I don’t know why but I spent a lot of time to find this topic.

// This doesn't work. Why?
// return tokenContext.processFlow(true, AUTHENTICATE_PATH, tokenContext.getRealm().getBrowserFlow(), null, new AuthenticationProcessor());

I’ve not found why that code doesn’t work. But you can change the first parameter to false (not an action) and in that case you will force browser flow.

    @Override
    public Response handleToken(OneTimeActionToken token, ActionTokenContext<OneTimeActionToken> tokenContext) {
        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
        UserModel user = authSession.getAuthenticatedUser();

        authSession.setRedirectUri(token.getRedirectUrl());
        authSession.setAuthNote(FastRegistrationAuthenticator.FAST_REGISTRATION_VERIFIED, user.getEmail());

        return tokenContext.processFlow(
                false,
                AUTHENTICATE_PATH,
                tokenContext.getRealm().getBrowserFlow(),
                null,
                new AuthenticationProcessor());
    }

Only one method it the authenticator should be overridden

	@Override
	public void authenticate(AuthenticationFlowContext context) {
		AuthenticationSessionModel authSession = context.getAuthenticationSession();
		UserModel user = context.getUser();

		if (Objects.equals(authSession.getAuthNote(FAST_REGISTRATION_VERIFIED), user.getEmail())) {
			context.success();
			return;
		}

		context.attempted();
	}