Merge pull request #2551 from stianst/KEYCLOAK-2683
KEYCLOAK-2683 Remove QRCodeResource and embed QR code in image
This commit is contained in:
commit
ed97a9b6f3
10 changed files with 100 additions and 169 deletions
|
@ -188,7 +188,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
|||
|
||||
switch (page) {
|
||||
case TOTP:
|
||||
attributes.put("totp", new TotpBean(session, realm, user, baseUri));
|
||||
attributes.put("totp", new TotpBean(session, realm, user));
|
||||
break;
|
||||
case FEDERATED_IDENTITY:
|
||||
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
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.Base32;
|
||||
import org.keycloak.models.utils.HmacOTP;
|
||||
import org.keycloak.utils.TotpUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
|
@ -34,35 +37,15 @@ public class TotpBean {
|
|||
|
||||
private final String totpSecret;
|
||||
private final String totpSecretEncoded;
|
||||
private final String totpSecretQrCode;
|
||||
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.contextUrl = baseUri.getPath();
|
||||
|
||||
this.totpSecret = randomString(20);
|
||||
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
||||
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, user, this.totpSecret);
|
||||
}
|
||||
|
||||
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();
|
||||
this.totpSecret = HmacOTP.generateSecret(20);
|
||||
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
|
@ -74,19 +57,11 @@ public class TotpBean {
|
|||
}
|
||||
|
||||
public String getTotpSecretEncoded() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
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();
|
||||
return totpSecretEncoded;
|
||||
}
|
||||
|
||||
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
||||
String contents = URLEncoder.encode(keyUri, "utf-8");
|
||||
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
||||
public String getTotpSecretQrCode() {
|
||||
return totpSecretQrCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -279,7 +279,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
|
||||
switch (page) {
|
||||
case LOGIN_CONFIG_TOTP:
|
||||
attributes.put("totp", new TotpBean(realm, user, baseUri));
|
||||
attributes.put("totp", new TotpBean(realm, user));
|
||||
break;
|
||||
case LOGIN_UPDATE_PROFILE:
|
||||
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.utils.Base32;
|
||||
import org.keycloak.models.utils.HmacOTP;
|
||||
import org.keycloak.utils.TotpUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
|
@ -32,17 +33,15 @@ public class TotpBean {
|
|||
|
||||
private final String totpSecret;
|
||||
private final String totpSecretEncoded;
|
||||
private final String totpSecretQrCode;
|
||||
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.contextUrl = baseUri.getPath();
|
||||
|
||||
|
||||
this.totpSecret = HmacOTP.generateSecret(20);
|
||||
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
||||
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, user, this.totpSecret);
|
||||
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
|
@ -54,19 +53,11 @@ public class TotpBean {
|
|||
}
|
||||
|
||||
public String getTotpSecretEncoded() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
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();
|
||||
return totpSecretEncoded;
|
||||
}
|
||||
|
||||
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
||||
String contents = URLEncoder.encode(keyUri, "utf-8");
|
||||
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
||||
public String getTotpSecretQrCode() {
|
||||
return totpSecretQrCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -82,7 +82,6 @@ public class KeycloakApplication extends Application {
|
|||
singletons.add(new ServerVersionResource());
|
||||
singletons.add(new RealmsResource());
|
||||
singletons.add(new AdminRoot());
|
||||
classes.add(QRCodeResource.class);
|
||||
classes.add(ThemeResource.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>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
@ -204,17 +204,20 @@ ol li {
|
|||
|
||||
ol li img {
|
||||
margin-top: 15px;
|
||||
width: 180px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
ol li span {
|
||||
bottom: 80px;
|
||||
left: 200px;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #eee;
|
||||
top: 46px;
|
||||
left: 270px;
|
||||
right: 50px;
|
||||
position: absolute;
|
||||
font-family: courier, monospace;
|
||||
font-size: 13px;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
hr + .form-horizontal {
|
||||
|
|
Loading…
Reference in a new issue