/*
* oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
*
* Copyright (c) 2014, Gluu
*/
package org.xdi.oxauth.service.fido.u2f;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.inject.Named;
import org.gluu.site.ldap.persistence.LdapEntryManager;
import org.slf4j.Logger;
import org.xdi.oxauth.crypto.random.ChallengeGenerator;
import org.xdi.oxauth.exception.fido.u2f.DeviceCompromisedException;
import org.xdi.oxauth.exception.fido.u2f.InvalidKeyHandleDeviceException;
import org.xdi.oxauth.exception.fido.u2f.NoEligableDevicesException;
import org.xdi.oxauth.model.config.StaticConfiguration;
import org.xdi.oxauth.model.fido.u2f.AuthenticateRequestMessageLdap;
import org.xdi.oxauth.model.fido.u2f.DeviceRegistration;
import org.xdi.oxauth.model.fido.u2f.DeviceRegistrationResult;
import org.xdi.oxauth.model.fido.u2f.exception.BadInputException;
import org.xdi.oxauth.model.fido.u2f.message.RawAuthenticateResponse;
import org.xdi.oxauth.model.fido.u2f.protocol.AuthenticateRequest;
import org.xdi.oxauth.model.fido.u2f.protocol.AuthenticateRequestMessage;
import org.xdi.oxauth.model.fido.u2f.protocol.AuthenticateResponse;
import org.xdi.oxauth.model.fido.u2f.protocol.ClientData;
import org.xdi.oxauth.model.util.Base64Util;
import org.xdi.oxauth.service.UserService;
import org.xdi.util.StringHelper;
import com.unboundid.ldap.sdk.Filter;
/**
* Provides operations with U2F authentication request
*
* @author Yuriy Movchan Date: 05/19/2015
*/
@Stateless
@Named("u2fAuthenticationService")
public class AuthenticationService extends RequestService {
@Inject
private Logger log;
@Inject
private LdapEntryManager ldapEntryManager;
@Inject
private ApplicationService applicationService;
@Inject
private RawAuthenticationService rawAuthenticationService;
@Inject
private ClientDataValidationService clientDataValidationService;
@Inject
private DeviceRegistrationService deviceRegistrationService;
@Inject
private UserService userService;
@Inject @Named("randomChallengeGenerator")
private ChallengeGenerator challengeGenerator;
@Inject
private StaticConfiguration staticConfiguration;
public AuthenticateRequestMessage buildAuthenticateRequestMessage(String appId, String userInum) throws BadInputException, NoEligableDevicesException {
if (applicationService.isValidateApplication()) {
applicationService.checkIsValid(appId);
}
List<AuthenticateRequest> authenticateRequests = new ArrayList<AuthenticateRequest>();
byte[] challenge = challengeGenerator.generateChallenge();
List<DeviceRegistration> deviceRegistrations = deviceRegistrationService.findUserDeviceRegistrations(userInum, appId);
for (DeviceRegistration deviceRegistration : deviceRegistrations) {
if (!deviceRegistration.isCompromised()) {
AuthenticateRequest request;
try {
request = startAuthentication(appId, deviceRegistration, challenge);
authenticateRequests.add(request);
} catch (DeviceCompromisedException ex) {
log.error("Faield to authenticate device", ex);
}
}
}
if (authenticateRequests.isEmpty()) {
if (deviceRegistrations.isEmpty()) {
throw new NoEligableDevicesException(deviceRegistrations, "No devices registrered");
} else {
throw new NoEligableDevicesException(deviceRegistrations, "All devices compromised");
}
}
return new AuthenticateRequestMessage(authenticateRequests);
}
public AuthenticateRequest startAuthentication(String appId, DeviceRegistration device) throws DeviceCompromisedException {
return startAuthentication(appId, device, challengeGenerator.generateChallenge());
}
public AuthenticateRequest startAuthentication(String appId, DeviceRegistration device, byte[] challenge) throws DeviceCompromisedException {
if (device.isCompromised()) {
throw new DeviceCompromisedException(device, "Device has been marked as compromised, cannot authenticate");
}
return new AuthenticateRequest(Base64Util.base64urlencode(challenge), appId, device.getKeyHandle());
}
public DeviceRegistrationResult finishAuthentication(AuthenticateRequestMessage requestMessage, AuthenticateResponse response, String userInum)
throws BadInputException, DeviceCompromisedException {
return finishAuthentication(requestMessage, response, userInum, null);
}
public DeviceRegistrationResult finishAuthentication(AuthenticateRequestMessage requestMessage, AuthenticateResponse response, String userInum, Set<String> facets)
throws BadInputException, DeviceCompromisedException {
List<DeviceRegistration> deviceRegistrations = deviceRegistrationService.findUserDeviceRegistrations(userInum, requestMessage.getAppId());
final AuthenticateRequest request = getAuthenticateRequest(requestMessage, response);
DeviceRegistration usedDeviceRegistration = null;
for (DeviceRegistration deviceRegistration : deviceRegistrations) {
if (StringHelper.equals(request.getKeyHandle(), deviceRegistration.getKeyHandle())) {
usedDeviceRegistration = deviceRegistration;
break;
}
}
if (usedDeviceRegistration == null) {
throw new BadInputException("Failed to find DeviceRegistration for the given AuthenticateRequest");
}
if (usedDeviceRegistration.isCompromised()) {
throw new DeviceCompromisedException(usedDeviceRegistration, "The device is marked as possibly compromised, and cannot be authenticated");
}
ClientData clientData = response.getClientData();
clientDataValidationService.checkContent(clientData, RawAuthenticationService.SUPPORTED_AUTHENTICATE_TYPES, request.getChallenge(), facets);
RawAuthenticateResponse rawAuthenticateResponse = rawAuthenticationService.parseRawAuthenticateResponse(response.getSignatureData());
rawAuthenticationService.checkSignature(request.getAppId(), clientData, rawAuthenticateResponse,
Base64Util.base64urldecode(usedDeviceRegistration.getDeviceRegistrationConfiguration().getPublicKey()));
rawAuthenticateResponse.checkUserPresence();
usedDeviceRegistration.checkAndUpdateCounter(rawAuthenticateResponse.getCounter());
usedDeviceRegistration.setLastAccessTime(new Date());
deviceRegistrationService.updateDeviceRegistration(userInum, usedDeviceRegistration);
DeviceRegistrationResult.Status status = DeviceRegistrationResult.Status.APPROVED;
boolean approved = StringHelper.equals(RawAuthenticationService.AUTHENTICATE_GET_TYPE, clientData.getTyp());
if (!approved) {
status = DeviceRegistrationResult.Status.CANCELED;
log.debug("Authentication request with keyHandle '{}' was canceled", response.getKeyHandle());
}
return new DeviceRegistrationResult(usedDeviceRegistration, status);
}
public AuthenticateRequest getAuthenticateRequest(AuthenticateRequestMessage requestMessage, AuthenticateResponse response) throws BadInputException {
if (!StringHelper.equals(requestMessage.getRequestId(), response.getRequestId())) {
throw new BadInputException("Wrong request for response data");
}
for (AuthenticateRequest request : requestMessage.getAuthenticateRequests()) {
if (StringHelper.equals(request.getKeyHandle(), response.getKeyHandle())) {
return request;
}
}
throw new BadInputException("Responses keyHandle does not match any contained request");
}
public void storeAuthenticationRequestMessage(AuthenticateRequestMessage requestMessage, String userInum, String sessionState) {
Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime();
final String authenticateRequestMessageId = UUID.randomUUID().toString();
AuthenticateRequestMessageLdap authenticateRequestMessageLdap = new AuthenticateRequestMessageLdap(getDnForAuthenticateRequestMessage(authenticateRequestMessageId),
authenticateRequestMessageId, now, sessionState, userInum, requestMessage);
ldapEntryManager.persist(authenticateRequestMessageLdap);
}
public AuthenticateRequestMessage getAuthenticationRequestMessage(String oxId) {
String requestDn = getDnForAuthenticateRequestMessage(oxId);
AuthenticateRequestMessageLdap authenticateRequestMessageLdap = ldapEntryManager.find(AuthenticateRequestMessageLdap.class, requestDn);
if (authenticateRequestMessageLdap == null) {
return null;
}
return authenticateRequestMessageLdap.getAuthenticateRequestMessage();
}
public AuthenticateRequestMessageLdap getAuthenticationRequestMessageByRequestId(String requestId) {
String baseDn = getDnForAuthenticateRequestMessage(null);
Filter requestIdFilter = Filter.createEqualityFilter("oxRequestId", requestId);
List<AuthenticateRequestMessageLdap> authenticateRequestMessagesLdap = ldapEntryManager.findEntries(baseDn, AuthenticateRequestMessageLdap.class,
requestIdFilter);
if ((authenticateRequestMessagesLdap == null) || authenticateRequestMessagesLdap.isEmpty()) {
return null;
}
return authenticateRequestMessagesLdap.get(0);
}
public void removeAuthenticationRequestMessage(AuthenticateRequestMessageLdap authenticateRequestMessageLdap) {
removeRequestMessage(authenticateRequestMessageLdap);
}
public String getUserInumByKeyHandle(String appId, String keyHandle) throws InvalidKeyHandleDeviceException {
if (org.xdi.util.StringHelper.isEmpty(appId) || StringHelper.isEmpty(keyHandle)) {
return null;
}
List<DeviceRegistration> deviceRegistrations = deviceRegistrationService.findDeviceRegistrationsByKeyHandle(appId, keyHandle, "oxId");
if (deviceRegistrations.isEmpty()) {
throw new InvalidKeyHandleDeviceException(String.format("Failed to find device by keyHandle '%s' in LDAP", keyHandle));
}
if (deviceRegistrations.size() != 1) {
throw new BadInputException(String.format("There are '%d' devices with keyHandle '%s' in LDAP", deviceRegistrations.size(), keyHandle));
}
DeviceRegistration deviceRegistration = deviceRegistrations.get(0);
return userService.getUserInumByDn(deviceRegistration.getDn());
}
/**
* Build DN string for U2F authentication request
*/
public String getDnForAuthenticateRequestMessage(String oxId) {
final String u2fBaseDn = staticConfiguration.getBaseDn().getU2fBase(); // ou=authentication_requests,ou=u2f,o=@!1111,o=gluu
if (StringHelper.isEmpty(oxId)) {
return String.format("ou=authentication_requests,%s", u2fBaseDn);
}
return String.format("oxid=%s,ou=authentication_requests,%s", oxId, u2fBaseDn);
}
}