我有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