Merge pull request #2551 from stianst/KEYCLOAK-2683

KEYCLOAK-2683 Remove QRCodeResource and embed QR code in image
This commit is contained in:
Stian Thorgersen 2016-04-08 09:53:28 +02:00
commit ed97a9b6f3
10 changed files with 100 additions and 169 deletions

View file

@ -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));

View 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;
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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();
}
}

View 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);
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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 {