keycloak-scim/server-spi-private/src/main/java/org/keycloak/authentication/ConsoleDisplayMode.java
2018-03-29 17:14:36 -04:00

324 lines
10 KiB
Java

package org.keycloak.authentication;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
* This class encapsulates a proprietary HTTP challenge protocol designed by keycloak team which is used by text-based console
* clients to dynamically render and prompt for information in a textual manner. The class is a builder which can
* build the challenge response (the header and response body).
*
* When doing code to token flow in OAuth, server could respond with
*
* 401
* WWW-Authenticate: X-Text-Form-Challenge callback="http://localhost/..."
* param="username" label="Username: " mask=false
* param="password" label="Password: " mask=true
* Content-Type: text/plain
*
* Please login with your username and password
*
*
* The client receives this challenge. It first outputs whatever the text body of the message contains. It will
* then prompt for username and password using the label values as prompt messages for each parameter.
*
* After the input has been entered by the user, the client does a form POST to the callback url with the values of the
* input parameters entered.
*
* The server can challenge with 401 as many times as it wants. The client will look for 302 responses. It will will
* follow all redirects unless the Location url has an OAuth "code" parameter. If there is a code parameter, then the
* client will stop and finish the OAuth flow to obtain a token. Any other response code other than 401 or 302 the client
* should abort with an error message.
*
*/
public class ConsoleDisplayMode {
/**
* Browser is required to login. This will abort client from doing a console login.
*
* @param session
* @return
*/
public static Response browserRequired(KeycloakSession session) {
return Response.status(Response.Status.UNAUTHORIZED)
.header("WWW-Authenticate", "X-Text-Form-Challenge browserRequired")
.type(MediaType.TEXT_PLAIN)
.entity("\n" + session.getProvider(LoginFormsProvider.class).getMessage("browserRequired") + "\n").build();
}
/**
* Browser is required to continue login. This will prompt client on whether to continue with a browser or abort.
*
* @param session
* @param callback
* @return
*/
public static Response browserContinue(KeycloakSession session, String callback) {
String browserContinueMsg = session.getProvider(LoginFormsProvider.class).getMessage("browserContinue");
String browserPrompt = session.getProvider(LoginFormsProvider.class).getMessage("browserContinuePrompt");
String answer = session.getProvider(LoginFormsProvider.class).getMessage("browserContinueAnswer");
String header = "X-Text-Form-Challenge callback=\"" + callback + "\"";
header += " browserContinue=\"" + browserPrompt + "\" answer=\"" + answer + "\"";
return Response.status(Response.Status.UNAUTHORIZED)
.header("WWW-Authenticate", header)
.type(MediaType.TEXT_PLAIN)
.entity("\n" + browserContinueMsg + "\n").build();
}
/**
* Build challenge response for required actions
*
* @param context
* @return
*/
public static ConsoleDisplayMode challenge(RequiredActionContext context) {
return new ConsoleDisplayMode(context);
}
/**
* Build challenge response for authentication flows
*
* @param context
* @return
*/
public static ConsoleDisplayMode challenge(AuthenticationFlowContext context) {
return new ConsoleDisplayMode(context);
}
/**
* Build challenge response header only for required actions
*
* @param context
* @return
*/
public static HeaderBuilder header(RequiredActionContext context) {
return new ConsoleDisplayMode(context).header();
}
/**
* Build challenge response header only for authentication flows
*
* @param context
* @return
*/
public static HeaderBuilder header(AuthenticationFlowContext context) {
return new ConsoleDisplayMode(context).header();
}
ConsoleDisplayMode(RequiredActionContext requiredActionContext) {
this.requiredActionContext = requiredActionContext;
}
ConsoleDisplayMode(AuthenticationFlowContext flowContext) {
this.flowContext = flowContext;
}
protected RequiredActionContext requiredActionContext;
protected AuthenticationFlowContext flowContext;
protected HeaderBuilder header;
/**
* Create a theme form pre-populated with challenge
*
* @return
*/
public LoginFormsProvider form() {
if (header == null) throw new RuntimeException("Header Not Set");
return formInternal()
.setStatus(Response.Status.UNAUTHORIZED)
.setMediaType(MediaType.TEXT_PLAIN_TYPE)
.setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, header.build());
}
/**
* Create challenge response with a body generated from localized
* message.properties of your theme
*
* @param msg message id
* @param params parameters to use to format the message
*
* @return
*/
public Response message(String msg, String... params) {
if (header == null) throw new RuntimeException("Header Not Set");
Response response = Response.status(401)
.header(HttpHeaders.WWW_AUTHENTICATE, header.build())
.type(MediaType.TEXT_PLAIN)
.entity("\n" + formInternal().getMessage(msg, params) + "\n").build();
return response;
}
/**
* Create challenge response with a text message body
*
* @param text plain text of http response body
*
* @return
*/
public Response text(String text) {
if (header == null) throw new RuntimeException("Header Not Set");
Response response = Response.status(401)
.header(HttpHeaders.WWW_AUTHENTICATE, header.build())
.type(MediaType.TEXT_PLAIN)
.entity("\n" + text + "\n").build();
return response;
}
/**
* Generate response with empty http response body
*
* @return
*/
public Response response() {
if (header == null) throw new RuntimeException("Header Not Set");
Response response = Response.status(401)
.header(HttpHeaders.WWW_AUTHENTICATE, header.build()).build();
return response;
}
protected LoginFormsProvider formInternal() {
if (requiredActionContext != null) {
return requiredActionContext.form();
} else {
return flowContext.form();
}
}
/**
* Start building the header
*
* @return
*/
public HeaderBuilder header() {
String callback;
if (requiredActionContext != null) {
callback = requiredActionContext.getActionUrl(true).toString();
} else {
callback = flowContext.getActionUrl(flowContext.generateAccessCode(), true).toString();
}
header = new HeaderBuilder(callback);
return header;
}
public class HeaderBuilder {
protected StringBuilder builder = new StringBuilder();
protected HeaderBuilder(String callback) {
builder.append("X-Text-Form-Challenge callback=\"").append(callback).append("\" ");
}
protected ParamBuilder param;
protected void checkParam() {
if (param != null) {
param.buildInternal();
param = null;
}
}
/**
* Build header string
*
* @return
*/
public String build() {
checkParam();
return builder.toString();
}
/**
* Define a param
*
* @param name
* @return
*/
public ParamBuilder param(String name) {
checkParam();
builder.append("param=\"").append(name).append("\" ");
param = new ParamBuilder(name);
return param;
}
public class ParamBuilder {
protected boolean mask;
protected String label;
protected ParamBuilder(String name) {
this.label = name;
}
public ParamBuilder label(String msg) {
this.label = formInternal().getMessage(msg);
return this;
}
public ParamBuilder labelText(String txt) {
this.label = txt;
return this;
}
/**
* Should input be masked by the client. For example, when entering password, you don't want to show password on console.
*
* @param mask
* @return
*/
public ParamBuilder mask(boolean mask) {
this.mask = mask;
return this;
}
public void buildInternal() {
builder.append("label=\"").append(label).append(" \" ");
builder.append("mask=").append(mask).append(" ");
}
/**
* Build header string
*
* @return
*/
public String build() {
return HeaderBuilder.this.build();
}
public ConsoleDisplayMode challenge() {
return ConsoleDisplayMode.this;
}
public LoginFormsProvider form() {
return ConsoleDisplayMode.this.form();
}
public Response message(String msg, String... params) {
return ConsoleDisplayMode.this.message(msg, params);
}
public Response text(String text) {
return ConsoleDisplayMode.this.text(text);
}
public ParamBuilder param(String name) {
return HeaderBuilder.this.param(name);
}
}
}
}