Skip to content

REST: Allow user with permission to manage existing user to generate magic link #145

@jungm

Description

@jungm

We have a use case that admittedly may be somewhat outside the scope of this extension, but besides a few minor adjustments in the MagicLinkResource I can see this working perfectly for us.

What we are trying to build is the following:

  • B2B customer gets access to keycloak admin api and can manage their users with fine grained admin authz, but they don't have global manage-users permissions across the entire realm
  • They can generate a magic link to redirect their users to which automatically creates/assigns a keycloak session and thus authenticates them

The tricky part is the magic link part, which you guys thankfully already solved!
To make this work I'd like to propose the following change to MagicLinkResource: Check if the email/username already exists, if so test if the user can manage this specific user and generate the magic link, basically just respecting whatever is configured using fine grained admin authz. If the user doesn't exist it IMO only makes sense to still test if the requesting user can manage all users, so no change needed in that path.

Here's a rough example I came up with of what I mean:

  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  public MagicLinkResponse createMagicLink(final MagicLinkRequest rep) {
    String emailOrUsername = rep.getEmail();
    boolean forceCreate = rep.isForceCreate();
    boolean sendEmail = rep.isSendEmail();

    if (rep.getUsername() != null) {
      emailOrUsername = rep.getUsername();
      forceCreate = false;
      sendEmail = false;
    }

    UserModel user = MagicLink.getOrCreate(session, realm, emailOrUsername, false,
            false, false, MagicLink.registerEvent(event, MAGIC_LINK));

    if (user != null) {
        if (!permissions.users().canManage(user))
          throw new ForbiddenException("can't manager user " + user.getUsername());
    } else {
        if (!forceCreate)
            throw new NotFoundException(
                    String.format(
                            "User with email/username %s not found, and forceCreate is off.", emailOrUsername));

        if (!permissions.users().canManage())
            throw new ForbiddenException("magic link requires manage-users");
    }

    ClientModel client = session.clients().getClientByClientId(realm, rep.getClientId());
    if (client == null)
      throw new NotFoundException(String.format("Client with ID %s not found.", rep.getClientId()));
    if (!MagicLink.validateRedirectUri(session, rep.getRedirectUri(), client))
      throw new BadRequestException(
          String.format("redirectUri %s disallowed by client.", rep.getRedirectUri()));

    // previous lookup failed, permission to manage all users is verified, so try to create the user now
    if (user == null)
    {
        user = MagicLink.getOrCreate(
                session,
                realm,
                emailOrUsername,
                true,
                rep.isUpdateProfile(),
                rep.isUpdatePassword(),
                MagicLink.registerEvent(event, MAGIC_LINK));
    }

    MagicLinkActionToken token =
        MagicLink.createActionToken(
            user,
            rep.getClientId(),
            rep.getRedirectUri(),
            OptionalInt.of(rep.getExpirationSeconds()),
            rep.getScope(),
            rep.getNonce(),
            rep.getState(),
            rep.getCodeChallenge(),
            rep.getCodeChallengeMethod(),
            rep.getRememberMe(),
            rep.getActionTokenPersistent());
    String link = MagicLink.linkFromActionToken(session, realm, token);
    boolean sent = false;
    if (sendEmail) {
      sent = MagicLink.sendMagicLinkEmail(session, user, link);
      log.debugf("sent email to %s? %b. Link? %s", rep.getEmail(), sent, link);
    }

    MagicLinkResponse resp = new MagicLinkResponse();
    resp.setUserId(user.getId());
    resp.setLink(link);
    resp.setSent(sent);

    return resp;
  }

Of course I'll create a PR for you if you think this is something that could fit in 🙂

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions