KEYCLOAK-2683 Remove QRCodeResource and embed QR code in image
This commit is contained in:
parent
1e1bf97ffc
commit
8ea057a122
10 changed files with 100 additions and 169 deletions
|
@ -188,7 +188,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case TOTP:
|
case TOTP:
|
||||||
attributes.put("totp", new TotpBean(session, realm, user, baseUri));
|
attributes.put("totp", new TotpBean(session, realm, user));
|
||||||
break;
|
break;
|
||||||
case FEDERATED_IDENTITY:
|
case FEDERATED_IDENTITY:
|
||||||
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
||||||
|
|
47
services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java
Executable file → Normal file
47
services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java
Executable file → Normal file
|
@ -14,12 +14,15 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.forms.account.freemarker.model;
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.Base32;
|
import org.keycloak.models.utils.Base32;
|
||||||
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
|
import org.keycloak.utils.TotpUtils;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -34,35 +37,15 @@ public class TotpBean {
|
||||||
|
|
||||||
private final String totpSecret;
|
private final String totpSecret;
|
||||||
private final String totpSecretEncoded;
|
private final String totpSecretEncoded;
|
||||||
|
private final String totpSecretQrCode;
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final String contextUrl;
|
|
||||||
private final String keyUri;
|
|
||||||
|
|
||||||
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri) {
|
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user);
|
this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user);
|
||||||
this.contextUrl = baseUri.getPath();
|
|
||||||
|
|
||||||
this.totpSecret = randomString(20);
|
this.totpSecret = HmacOTP.generateSecret(20);
|
||||||
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||||
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, user, this.totpSecret);
|
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||||
}
|
|
||||||
|
|
||||||
private static String randomString(int length) {
|
|
||||||
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890";
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
char c = chars.charAt(random.nextInt(chars.length()));
|
|
||||||
sb.append(c);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final SecureRandom random;
|
|
||||||
|
|
||||||
static
|
|
||||||
{
|
|
||||||
random = new SecureRandom();
|
|
||||||
random.nextInt();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
|
@ -74,19 +57,11 @@ public class TotpBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTotpSecretEncoded() {
|
public String getTotpSecretEncoded() {
|
||||||
StringBuilder sb = new StringBuilder();
|
return totpSecretEncoded;
|
||||||
for (int i = 0; i < totpSecretEncoded.length(); i += 4) {
|
|
||||||
sb.append(totpSecretEncoded.substring(i, i + 4 < totpSecretEncoded.length() ? i + 4 : totpSecretEncoded.length()));
|
|
||||||
if (i + 4 < totpSecretEncoded.length()) {
|
|
||||||
sb.append(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
public String getTotpSecretQrCode() {
|
||||||
String contents = URLEncoder.encode(keyUri, "utf-8");
|
return totpSecretQrCode;
|
||||||
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,7 +279,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case LOGIN_CONFIG_TOTP:
|
case LOGIN_CONFIG_TOTP:
|
||||||
attributes.put("totp", new TotpBean(realm, user, baseUri));
|
attributes.put("totp", new TotpBean(realm, user));
|
||||||
break;
|
break;
|
||||||
case LOGIN_UPDATE_PROFILE:
|
case LOGIN_UPDATE_PROFILE:
|
||||||
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
|
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.Base32;
|
import org.keycloak.models.utils.Base32;
|
||||||
import org.keycloak.models.utils.HmacOTP;
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
|
import org.keycloak.utils.TotpUtils;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -32,17 +33,15 @@ public class TotpBean {
|
||||||
|
|
||||||
private final String totpSecret;
|
private final String totpSecret;
|
||||||
private final String totpSecretEncoded;
|
private final String totpSecretEncoded;
|
||||||
|
private final String totpSecretQrCode;
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final String contextUrl;
|
|
||||||
private final String keyUri;
|
|
||||||
|
|
||||||
public TotpBean(RealmModel realm, UserModel user, URI baseUri) {
|
public TotpBean(RealmModel realm, UserModel user) {
|
||||||
this.enabled = user.isOtpEnabled();
|
this.enabled = user.isOtpEnabled();
|
||||||
this.contextUrl = baseUri.getPath();
|
|
||||||
|
|
||||||
this.totpSecret = HmacOTP.generateSecret(20);
|
this.totpSecret = HmacOTP.generateSecret(20);
|
||||||
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||||
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, user, this.totpSecret);
|
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
|
@ -54,19 +53,11 @@ public class TotpBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTotpSecretEncoded() {
|
public String getTotpSecretEncoded() {
|
||||||
StringBuilder sb = new StringBuilder();
|
return totpSecretEncoded;
|
||||||
for (int i = 0; i < totpSecretEncoded.length(); i += 4) {
|
|
||||||
sb.append(totpSecretEncoded.substring(i, i + 4 < totpSecretEncoded.length() ? i + 4 : totpSecretEncoded.length()));
|
|
||||||
if (i + 4 < totpSecretEncoded.length()) {
|
|
||||||
sb.append(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
public String getTotpSecretQrCode() {
|
||||||
String contents = URLEncoder.encode(keyUri, "utf-8");
|
return totpSecretQrCode;
|
||||||
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,6 @@ public class KeycloakApplication extends Application {
|
||||||
singletons.add(new ServerVersionResource());
|
singletons.add(new ServerVersionResource());
|
||||||
singletons.add(new RealmsResource());
|
singletons.add(new RealmsResource());
|
||||||
singletons.add(new AdminRoot());
|
singletons.add(new AdminRoot());
|
||||||
classes.add(QRCodeResource.class);
|
|
||||||
classes.add(ThemeResource.class);
|
classes.add(ThemeResource.class);
|
||||||
classes.add(JsResource.class);
|
classes.add(JsResource.class);
|
||||||
|
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.keycloak.services.resources;
|
|
||||||
|
|
||||||
import com.google.zxing.BarcodeFormat;
|
|
||||||
import com.google.zxing.WriterException;
|
|
||||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
|
||||||
import com.google.zxing.common.BitMatrix;
|
|
||||||
import com.google.zxing.qrcode.QRCodeWriter;
|
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.ws.rs.GET;
|
|
||||||
import javax.ws.rs.Path;
|
|
||||||
import javax.ws.rs.Produces;
|
|
||||||
import javax.ws.rs.QueryParam;
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
|
||||||
import javax.ws.rs.core.CacheControl;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import javax.ws.rs.core.StreamingOutput;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a barcode image
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
@Path("/qrcode")
|
|
||||||
public class QRCodeResource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a bar code image
|
|
||||||
*
|
|
||||||
* @param contents
|
|
||||||
* @param size
|
|
||||||
* @return
|
|
||||||
* @throws ServletException
|
|
||||||
* @throws IOException
|
|
||||||
* @throws WriterException
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Produces("image/png")
|
|
||||||
public Response createQrCode(@QueryParam("contents") String contents, @QueryParam("size") String size) throws ServletException, IOException, WriterException {
|
|
||||||
int width = 256;
|
|
||||||
int height = 256;
|
|
||||||
|
|
||||||
if (size != null) {
|
|
||||||
String[] s = size.split("x");
|
|
||||||
try {
|
|
||||||
width = Integer.parseInt(s[0]);
|
|
||||||
height = Integer.parseInt(s[1]);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contents == null) {
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width > 1000 || height > 1000 || contents.length() > 1000) {
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
QRCodeWriter writer = new QRCodeWriter();
|
|
||||||
final BitMatrix bitMatrix = writer.encode(contents, BarcodeFormat.QR_CODE, width, height);
|
|
||||||
|
|
||||||
StreamingOutput stream = new StreamingOutput() {
|
|
||||||
@Override
|
|
||||||
public void write(OutputStream os) throws IOException,
|
|
||||||
WebApplicationException {
|
|
||||||
MatrixToImageWriter.writeToStream(bitMatrix, "png", os);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This response is served with extra headers that tell the browser to not do any caching.
|
|
||||||
* The reason is that this page will include a QR code that can give an attacker access to
|
|
||||||
* the time based tokens, so it's best to take precautions and make sure there are no copies
|
|
||||||
* of the QR code lost in a cache.
|
|
||||||
*/
|
|
||||||
CacheControl cacheControl = CacheControlUtil.noCache();
|
|
||||||
|
|
||||||
return Response.ok(stream) //
|
|
||||||
.cacheControl(cacheControl) //
|
|
||||||
.header("Pragma","no-cache") //
|
|
||||||
.header("Expires", "0") //
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
69
services/src/main/java/org/keycloak/utils/TotpUtils.java
Normal file
69
services/src/main/java/org/keycloak/utils/TotpUtils.java
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.utils;
|
||||||
|
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||||
|
import com.google.zxing.common.BitMatrix;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.Base32;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class TotpUtils {
|
||||||
|
|
||||||
|
public static String encode(String totpSecret) {
|
||||||
|
String encoded = Base32.encode(totpSecret.getBytes());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < encoded.length(); i += 4) {
|
||||||
|
sb.append(encoded.substring(i, i + 4 < encoded.length() ? i + 4 : encoded.length()));
|
||||||
|
if (i + 4 < encoded.length()) {
|
||||||
|
sb.append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String qrCode(String totpSecret, RealmModel realm, UserModel user) {
|
||||||
|
try {
|
||||||
|
String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret);
|
||||||
|
|
||||||
|
int width = 246;
|
||||||
|
int height = 246;
|
||||||
|
|
||||||
|
QRCodeWriter writer = new QRCodeWriter();
|
||||||
|
final BitMatrix bitMatrix = writer.encode(keyUri, BarcodeFormat.QR_CODE, width, height);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
MatrixToImageWriter.writeToStream(bitMatrix, "png", bos);
|
||||||
|
bos.close();
|
||||||
|
|
||||||
|
return Base64.encodeBytes(bos.toByteArray());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -30,7 +30,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>${msg("totpStep2")}</p>
|
<p>${msg("totpStep2")}</p>
|
||||||
<img src="${totp.totpSecretQrCodeUrl}" alt="Figure: Barcode"><br/>
|
<img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
|
||||||
<span class="code">${totp.totpSecretEncoded}</span>
|
<span class="code">${totp.totpSecretEncoded}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>${msg("loginTotpStep2")}</p>
|
<p>${msg("loginTotpStep2")}</p>
|
||||||
<img src="${totp.totpSecretQrCodeUrl}" alt="Figure: Barcode"><br/>
|
<img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
|
||||||
<span class="code">${totp.totpSecretEncoded}</span>
|
<span class="code">${totp.totpSecretEncoded}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -204,17 +204,20 @@ ol li {
|
||||||
|
|
||||||
ol li img {
|
ol li img {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
width: 180px;
|
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol li span {
|
ol li span {
|
||||||
bottom: 80px;
|
padding: 15px;
|
||||||
left: 200px;
|
background-color: #f5f5f5;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
top: 46px;
|
||||||
|
left: 270px;
|
||||||
|
right: 50px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-family: courier, monospace;
|
font-family: courier, monospace;
|
||||||
font-size: 13px;
|
font-size: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr + .form-horizontal {
|
hr + .form-horizontal {
|
||||||
|
|
Loading…
Reference in a new issue