package com.intrbiz.bergamot.ui.router;
import java.io.IOException;
import java.io.OutputStream;
import java.util.UUID;
import java.util.stream.Collectors;
import com.intrbiz.Util;
import com.intrbiz.balsa.engine.route.Router;
import com.intrbiz.balsa.metadata.WithDataAdapter;
import com.intrbiz.bergamot.data.BergamotDB;
import com.intrbiz.bergamot.metadata.GetBergamotSite;
import com.intrbiz.bergamot.model.APIToken;
import com.intrbiz.bergamot.model.Contact;
import com.intrbiz.bergamot.model.ContactHOTPRegistration;
import com.intrbiz.bergamot.model.ContactU2FDeviceRegistration;
import com.intrbiz.bergamot.model.Site;
import com.intrbiz.bergamot.ui.BergamotApp;
import com.intrbiz.metadata.Any;
import com.intrbiz.metadata.AsString;
import com.intrbiz.metadata.CheckStringLength;
import com.intrbiz.metadata.CurrentPrincipal;
import com.intrbiz.metadata.Get;
import com.intrbiz.metadata.IsaUUID;
import com.intrbiz.metadata.Param;
import com.intrbiz.metadata.Prefix;
import com.intrbiz.metadata.RequireValidPrincipal;
import com.intrbiz.metadata.Template;
import com.intrbiz.util.CounterHOTP;
import com.intrbiz.util.HOTP.HOTPSecret;
import com.yubico.u2f.U2F;
import com.yubico.u2f.attestation.Attestation;
import com.yubico.u2f.attestation.MetadataService;
import com.yubico.u2f.data.DeviceRegistration;
import com.yubico.u2f.data.messages.RegisterRequestData;
import com.yubico.u2f.data.messages.RegisterResponse;
import net.glxn.qrgen.core.image.ImageType;
import net.glxn.qrgen.javase.QRCode;
@Prefix("/profile")
@Template("layout/main")
@RequireValidPrincipal()
public class ProfileRouter extends Router<BergamotApp>
{
private final U2F u2f = new U2F();
private final MetadataService u2fMetadata = new MetadataService();
private final CounterHOTP hotp = new CounterHOTP();
@Any("/")
@WithDataAdapter(BergamotDB.class)
public void index(BergamotDB db, @GetBergamotSite() Site site)
{
// generate a new U2F registration token
Contact contact = currentPrincipal();
RegisterRequestData registerRequestData = this.u2f.startRegistration(site.getU2FAppId(), contact.getU2FDeviceRegistrations().stream().map(ContactU2FDeviceRegistration::toDeviceRegistration).collect(Collectors.toList()));
var("u2fregister", registerRequestData);
// encode the profile view
encode("profile/index");
}
@Any("/revoke-api-token")
@WithDataAdapter(BergamotDB.class)
public void revokeAPIToken(BergamotDB db, @Param("token") @CheckStringLength(mandatory = true) String token) throws IOException
{
APIToken apiToken = db.getAPIToken(token);
if (apiToken != null)
{
db.setAPIToken(apiToken.revoke());
}
redirect(path("/profile/"));
}
@Any("/remove-api-token")
@WithDataAdapter(BergamotDB.class)
public void removeAPIToken(BergamotDB db, @Param("token") @CheckStringLength(mandatory = true) String token) throws IOException
{
db.removeAPIToken(token);
redirect(path("/profile/"));
}
@Any("/generate-api-token")
@WithDataAdapter(BergamotDB.class)
public void generateAPIToken(BergamotDB db, @Param("summary") @AsString() String summary) throws IOException
{
String token = app().getSecurityEngine().generatePerpetualAuthenticationTokenForPrincipal(currentPrincipal());
db.setAPIToken(new APIToken(token, currentPrincipal(), Util.coalesceEmpty(summary, "API Access")));
redirect(path("/profile/"));
}
@Any("/register-u2f-device")
@WithDataAdapter(BergamotDB.class)
public void registerU2FDevice(BergamotDB db, @GetBergamotSite() Site site, @Param("u2f-register-request") String request, @Param("u2f-register-response") String response, @Param("summary") String summary) throws Exception
{
// the contact
Contact contact = currentPrincipal();
// the name
String name = Util.coalesceEmpty(summary, "Security Key " + (contact.getHOTPRegistrations().size() + 1));
// register
DeviceRegistration reg = this.u2f.finishRegistration(RegisterRequestData.fromJson(request), RegisterResponse.fromJson(response));
// lookup the device metadata
Attestation attestation = this.u2fMetadata.getAttestation(reg.getAttestationCertificate());
System.out.println("Trusted: " + (attestation == null ? null : attestation.isTrusted()) + " " + attestation);
// store the registration
db.setU2FDeviceRegistration(new ContactU2FDeviceRegistration(
contact,
reg,
attestation == null ? null : attestation.getVendorProperties().get("name"),
attestation == null ? null : attestation.getDeviceProperties().get("displayName"),
attestation == null ? null : attestation.getDeviceProperties().get("imageUrl"),
name
));
// do we need more backup codes
contact.generateMoreBackupCodes();
// notifications
action("u2fa-device-registered", contact, name, attestation == null ? "Unknown Device" : attestation.getDeviceProperties().get("displayName"));
// done
redirect(path("/profile/"));
}
@Any("/revoke-u2f-device")
@WithDataAdapter(BergamotDB.class)
public void revokeU2FDevice(BergamotDB db, @Param("id") @IsaUUID UUID id) throws IOException
{
ContactU2FDeviceRegistration device = db.getU2FDeviceRegistration(id);
if (device != null)
{
db.setU2FDeviceRegistration(device.revoke());
}
redirect(path("/profile/"));
}
@Any("/remove-u2f-device")
@WithDataAdapter(BergamotDB.class)
public void removeU2FDevice(BergamotDB db, @Param("id") @IsaUUID UUID id) throws IOException
{
db.removeU2FDeviceRegistration(id);
redirect(path("/profile/"));
}
@Any("/revoke-hotp")
@WithDataAdapter(BergamotDB.class)
public void revokeHOTPDevice(BergamotDB db, @Param("id") @IsaUUID UUID id) throws IOException
{
ContactHOTPRegistration device = db.getHOTPRegistration(id);
if (device != null)
{
db.setHOTPRegistration(device.revoke());
}
redirect(path("/profile/"));
}
@Any("/remove-hotp")
@WithDataAdapter(BergamotDB.class)
public void removeHOTPDevice(BergamotDB db, @Param("id") @IsaUUID UUID id) throws IOException
{
db.removeHOTPRegistration(id);
redirect(path("/profile/"));
}
@Any("/setup-hotp")
@WithDataAdapter(BergamotDB.class)
public void setupHOTPDevice(BergamotDB db, @GetBergamotSite() Site site, @Param("summary") String summary) throws Exception
{
// the contact
Contact contact = currentPrincipal();
// the name
String name = Util.coalesceEmpty(summary, "Authenticator " + (contact.getHOTPRegistrations().size() + 1));
// generate a HOTP secret
HOTPSecret secret = this.hotp.newOTPSecret();
// store our HOTP registration
ContactHOTPRegistration registration = var("hotp", new ContactHOTPRegistration(contact, secret, name));
db.setHOTPRegistration(registration);
// do we need more backup codes
contact.generateMoreBackupCodes();
// notifications
action("u2fa-device-registered", contact, name, "HOTP Authenticator");
// done
encode("/profile/setuphotp");
}
@Get("/hotp-qr-code")
@WithDataAdapter(BergamotDB.class)
public void generateHOTPQRCode(BergamotDB db, @Param("id") @IsaUUID UUID hotpRegistrationId, @CurrentPrincipal Contact contact, @GetBergamotSite Site site) throws IOException
{
// get the HOTP registration
ContactHOTPRegistration registration = notNull(db.getHOTPRegistration(hotpRegistrationId));
// require that this registration is owned by the current principal
require(registration.getContactId().equals(contact.getId()));
// generate the QR Code
String account = Util.urlEncode(contact.getName(), Util.UTF8);
String issuer = Util.urlEncode(site.getSummary(), Util.UTF8);
String secret = registration.getHOTPSecret().toString();
String otpQRData = "otpauth://hotp/" + account + "?secret=" + secret + "&issuer=" + issuer + "&algorithm=SHA1&digits=6&counter=0";
// generate the QR code
OutputStream stream = response().ok().contentType("image/png").getOutput();
QRCode.from(otpQRData).withCharset("UTF-8").withSize(350, 350).to(ImageType.PNG).writeTo(stream);
}
@Any("/more-backup-codes")
@WithDataAdapter(BergamotDB.class)
public void moreBackupCodes(BergamotDB db) throws IOException
{
Contact contact = currentPrincipal();
contact.generateMoreBackupCodes();
redirect(path("/profile/"));
}
}