密钥斗篷从外部应用程序返回到身份验证流程

时间:2018-09-04 11:49:14

标签: java authentication keycloak

我有keycloak自定义SPI和自定义Action处理程序。我使用编码的callbackUrl创建QR图像,用户应在手机上扫描QR图像后返回。当我在同一主机上模拟它时,端口什么都可以,但是当我离开时说另一个端口,并从该应用程序triyng使用相同的URL返回,Keycloak返回“页面已过期”响应。我想是因为新来源的AUTH_SESSION_ID为空

身份验证者

@Override
    public void authenticate(AuthenticationFlowContext context) {
        logger.info("authenticate called ... context = " + context);
        try {
             String submitActionTokenUrl = generateCallbackUrl(context, "command0");
             final String base64String = getBase64QR(submitActionTokenUrl, "command0");
             Response challenge = context.form()
                    .setAttribute("totpSecretQrCode", base64String)
                    .setAttribute("manualUrl", submitActionTokenUrl)
                    .createForm("qr-validation.ftl");
            context.challenge(challenge);

        } catch (Exception e) {
            logger.info("{}", e);
            Response challenge = context.form()
                    .setError("Failed to generate QR-code")
                    .createForm("validation-error.ftl");
            context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR, challenge);
}




@Override
    public void action(AuthenticationFlowContext context) {
        logger.info("action called ... context = " + context);
        final AuthenticationSessionModel authSession = context.getAuthenticationSession();
        if (!Objects.equals(authSession.getAuthNote(ExternalApplicationNotificationActionTokenHandler.INITIATED_BY_ACTION_TOKEN_EXT_APP), "true")) {
            authenticate(context);
            return;
        }
        logger.info("OKKKKKKKKKKKKKKK" + context);
        authSession.removeAuthNote(ExternalApplicationNotificationActionTokenHandler.INITIATED_BY_ACTION_TOKEN_EXT_APP);
        String commandString = context.getUriInfo().getQueryParameters().getFirst("command");
        if (commandString.equals("command1")) {
            Response challenge = Response
                    .status(Response.Status.OK)
                    .header("Content-type", "application/json")
                    .entity("KEYCLAOK-REPLY-command1")
                    .build();

            context.challenge(challenge);
        } else if (commandString.equals("command2")) {
            Response challenge = Response
                    .status(Response.Status.OK)
                    .header("Content-type", "application/json")
                    .entity("KEYCLAOK-REPLY-command2")
                    .build();
            context.challenge(challenge);
        } else if (commandString.equals("SUCCESS")) {
            context.success();
        } else {
            context.failure(AuthenticationFlowError.INTERNAL_ERROR);
        }


 private String generateApplicationToken() throws IOException {
        JsonWebToken tokenSentBack = new JsonWebToken();
        SecretKeySpec hmacSecretKeySpec = new SecretKeySpec(org.keycloak.common.util.Base64.decode(SECRET), "HmacSHA256");
        String appToken = new JWSBuilder().jsonContent(tokenSentBack).hmac256(hmacSecretKeySpec);
        return URLEncoder.encode(appToken, "UTF-8");
    }

    private String generateActionToken(AuthenticationFlowContext context) {
        int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan();
        int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
        final AuthenticationSessionModel authSession = context.getAuthenticationSession();
        final String clientId = authSession.getClient().getClientId();
        ExternalApplicationNotificationActionToken actionToken = new ExternalApplicationNotificationActionToken(
                context.getUser().getId(),
                absoluteExpirationInSecs,
                clientId,
                KeycloakRemoteAuthenticatorFactory.PROVIDER_ID
        );
        KeycloakSession session = context.getSession();
        RealmModel realmModel = context.getRealm();
        UriInfo uriInfo = context.getUriInfo();
        return actionToken.serialize(
                session,
                realmModel,
                uriInfo
        );
    }


    private String generateCallbackUrl(AuthenticationFlowContext context, String command) throws IOException {
        final AuthenticationSessionModel authSession = context.getAuthenticationSession();
        final String clientId = authSession.getClient().getClientId();
        String applicationToken = generateApplicationToken();
        String actionToken = generateActionToken(context);
        return Urls
                .actionTokenBuilder(context.getUriInfo().getBaseUri(), actionToken, clientId, authSession.getTabId())
                .queryParam(Constants.EXECUTION, context.getExecution().getId())
                .queryParam(QUERY_PARAM_APP_TOKEN, "{tokenParameterName}")
                .queryParam("command", command)
                .build(context.getRealm().getName(), applicationToken)
                .toString();
    }

生成QR图像时,我还会生成用于手动单击的URL(以避免在开发过程中使用移动设备)。我创建了简单的MobileSimulator,它只是将一些请求发送回keycloak

移动模拟器:

private String request(Data data, String command) {
        HttpHeaders headers = new HttpHeaders();
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(callbackURL)
                .queryParam("key", data.key)
                .queryParam("client_id", data.clientId)
                .queryParam("tab_id", data.tabId)
                .queryParam("app-token", data.appToken)
                .queryParam("command", command)

        HttpEntity<?> entity = new HttpEntity<>(headers);

        HttpEntity<String> response = restTemplate.exchange(
                builder.toUriString(),
                HttpMethod.GET,
                entity,
                String.class);
        response.body
}

以下是示例或生成的网址:

http://localhost:8083/scan?key=eyJhbGciOiJIUzUxMiIsImtpZCIgOiAiNjZhMDY0MjctMjgyZC00ZmZmLTgxODYtYTQ4NmM0MTJjODkxIn0.eyJqdGkiOiJiZTBjNDAzZi04OWFhLTQ0NzktYjg2Ny1hOWNkNDAzYzg0N2EiLCJleHAiOjE1MzYwNDk1OTgsIm5iZiI6MCwiaWF0IjoxNTM2MDQ5Mjk4LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdmFzY28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdmFzY28iLCJzdWIiOiIxZTlkNGYxMS0yYmJmLTQ5ZDItODkyNC02NGJlYjRhZGEyZWMiLCJ0eXAiOiJleHRlcm5hbC1hcHAtbm90aWZpY2F0aW9uIiwibm9uY2UiOiJiZTBjNDAzZi04OWFhLTQ0NzktYjg2Ny1hOWNkNDAzYzg0N2EiLCJhcHAtaWQiOiJ2YXNjby1yZW1vdGUtYXV0aGVudGljYXRpb24iLCJhc2lkIjoidmFzY28tY2xpZW50IiwiYXNpZCI6InZhc2NvLWNsaWVudCJ9.384l47tk8ehkbUBWyCcpOcj5t-inREEwtcNwNTcdMlzkjoZqVtYJKVBEQ2wC3taFcS8oPOvdvB_vVCAGWuJOMA&client_id=vasco-client&tab_id=Y3h4BFVaTQk&execution=cc19e1d7-1b7f-4d69-aeb3-034541aee734&app-token=eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjAsIm5iZiI6MCwiaWF0IjowfQ.Q3PcCURqvGI9dLI-VPy9ZwE4-RcVsGW8im7kfPZlXdY&command=command0

这是模拟器尝试执行的请求示例:

http://localhost:8080/auth/realms/vasco/login-actions/action-token?key=eyJhbGciOiJIUzUxMiIsImtpZCIgOiAiNjZhMDY0MjctMjgyZC00ZmZmLTgxODYtYTQ4NmM0MTJjODkxIn0.eyJqdGkiOiJiZTBjNDAzZi04OWFhLTQ0NzktYjg2Ny1hOWNkNDAzYzg0N2EiLCJleHAiOjE1MzYwNDk1OTgsIm5iZiI6MCwiaWF0IjoxNTM2MDQ5Mjk4LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdmFzY28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdmFzY28iLCJzdWIiOiIxZTlkNGYxMS0yYmJmLTQ5ZDItODkyNC02NGJlYjRhZGEyZWMiLCJ0eXAiOiJleHRlcm5hbC1hcHAtbm90aWZpY2F0aW9uIiwibm9uY2UiOiJiZTBjNDAzZi04OWFhLTQ0NzktYjg2Ny1hOWNkNDAzYzg0N2EiLCJhcHAtaWQiOiJ2YXNjby1yZW1vdGUtYXV0aGVudGljYXRpb24iLCJhc2lkIjoidmFzY28tY2xpZW50IiwiYXNpZCI6InZhc2NvLWNsaWVudCJ9.384l47tk8ehkbUBWyCcpOcj5t-inREEwtcNwNTcdMlzkjoZqVtYJKVBEQ2wC3taFcS8oPOvdvB_vVCAGWuJOMA&client_id=vasco-client&tab_id=Y3h4BFVaTQk&execution=cc19e1d7-1b7f-4d69-aeb3-034541aee734&app-token=eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjAsIm5iZiI6MCwiaWF0IjowfQ.Q3PcCURqvGI9dLI-VPy9ZwE4-RcVsGW8im7kfPZlXdY&command=SUCCESS

0 个答案:

没有答案