From c013b161f2eb47f5952cbb80c8740f8d62d302c3 Mon Sep 17 00:00:00 2001 From: Yuriy Movchan Date: Fri, 27 Jan 2023 15:33:04 +0300 Subject: [PATCH] feat: Support Super Gluu one step authentication to Fido2 server #3593 (#3599) * feat: Support Super Gluu one step authentication to Fido2 server #3593 * feat: add sample request/response for one/two steps * feat: enrollment proxy for Super Gluu * chore: allow to process Super Gluu auth request * feat: add super gluu authentication flow support * feat: update to conform Jans * feat: update SG script and services to conform Fido2 server * feat: add fido2 device registration services to jans-auth-server * feat: full 2 step Super Gluu support * feat: user filter to search user's devices for specifc domain * fix: super_gluu_script * fix: super Gluu script * feat: support one_step Super Gluu enrollment * feat: clean up jans-auth-server static config * Revert "fix: super_gluu_script" This reverts commit f0e1713681d606b5d4c7a69d780610355546470b. * Revert "fix: super Gluu script" This reverts commit 20512c435845c180b8a55a0764453abe86d66ce5. * feat: super Gluu uses applicationId isntead of applicationId domain * feat: support Super Gluu one_step authentication * feat: add separate base DN for one step auth requests * feat: add super Fluu config option and disable it's API by default * feat: fixes in two step flow to conform katest API * feat move generic attributes to base bean * feat: remove unused services * chore: review script * chore: code review * chore: fix formatting * feat: add missing base fido2 branch * chore: code review * chore: review validators * feat: move Super Gluu adaptors code to separate services * chore: optimizations * chore: remove unused methods * feat: remove U2F clean up jobs * feat: more input parameters validations * feat: final optimizations and fixes Co-authored-by: Madhumita --- .../SuperGluuExternalAuthenticator.py | 283 ++++++++++-------- .../as/common/service/AttributeService.java | 3 - .../fido2/RegistrationPersistenceService.java | 200 +++++++++++++ .../as/model/config/BaseDnConfiguration.java | 21 ++ .../jans/as/server/service/CleanerTimer.java | 3 - .../as/server/service/SessionIdService.java | 13 +- .../CheckSessionStatusRestWebServiceImpl.java | 9 +- .../server/src/main/webapp/js/gluu-auth.js | 2 +- .../io/jans/fido2/ctap/AttestationFormat.java | 3 +- .../fido2/model/conf/AppConfiguration.java | 25 +- .../java/io/jans/fido2/sg/SuperGluuMode.java | 43 +++ .../service/AuthenticatorDataParser.java | 4 +- .../fido2/service/CertificateService.java | 23 +- .../fido2/service/ChallengeGenerator.java | 11 + .../jans/fido2/service/DataMapperService.java | 36 ++- .../io/jans/fido2/service/DigestService.java | 33 ++ .../service/operation/AssertionService.java | 149 ++++++--- .../service/operation/AttestationService.java | 98 ++++-- .../AuthenticationPersistenceService.java | 87 ++++-- .../RegistrationPersistenceService.java | 172 +++++------ .../service/persist/UserSessionIdService.java | 134 +++++++++ .../AppleAssertionFormatProcessor.java | 2 +- .../PackedAssertionFormatProcessor.java | 2 +- .../U2FAssertionFormatProcessor.java | 2 +- .../U2FSuperGluuAssertionFormatProcessor.java | 113 +++++++ .../U2FSuperGluuAttestationProcessor.java | 132 ++++++++ .../processors/AssertionFormatProcessor.java | 2 +- .../service/sg/RawAuthenticationService.java | 58 ++++ .../service/sg/RawRegistrationService.java | 88 ++++++ .../AssertionSuperGluuController.java | 228 ++++++++++++++ .../AttestationSuperGluuController.java | 276 +++++++++++++++++ .../service/shared/CustomScriptService.java | 1 - .../fido2/service/shared/MetricService.java | 6 +- .../service/verifier/AssertionVerifier.java | 2 +- .../verifier/AuthenticatorDataVerifier.java | 13 +- .../service/verifier/CommonVerifiers.java | 67 ++++- .../verifier/UserVerificationVerifier.java | 4 +- .../ws/rs/controller/AssertionController.java | 75 ++++- .../rs/controller/AttestationController.java | 56 +++- .../controller/ConfigurationController.java | 5 + .../persist/DeviceRegistrationService.java | 4 +- .../jans_setup/schema/jans_schema.json | 23 +- .../jans_setup/static/opendj/index.json | 7 + .../jans_setup/static/rdbm/mysql_index.json | 20 +- .../jans_setup/static/rdbm/pgsql_index.json | 20 +- .../jans_setup/static/rdbm/spanner_index.json | 20 +- .../jans_setup/templates/base.ldif | 17 +- .../jans-auth/jans-auth-static-conf.json | 1 - .../templates/jans-fido2/dynamic-conf.json | 2 + .../templates/jans-fido2/static-conf.json | 7 +- .../model/fido2}/Fido2AuthenticationData.java | 22 +- .../fido2}/Fido2AuthenticationEntry.java | 14 +- .../fido2}/Fido2AuthenticationStatus.java | 2 +- .../jans/orm/model/fido2/Fido2DeviceData.java | 101 +++++++ .../io/jans/orm/model/fido2/Fido2Entry.java | 93 +++++- .../model/fido2/Fido2RegistrationData.java | 2 +- .../model/fido2/Fido2RegistrationEntry.java | 47 +-- .../orm/model/fido2}/UserVerification.java | 2 +- 58 files changed, 2434 insertions(+), 454 deletions(-) create mode 100644 jans-auth-server/common/src/main/java/io/jans/as/common/service/common/fido2/RegistrationPersistenceService.java create mode 100644 jans-fido2/model/src/main/java/io/jans/fido2/sg/SuperGluuMode.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/DigestService.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/persist/UserSessionIdService.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FSuperGluuAssertionFormatProcessor.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/U2FSuperGluuAttestationProcessor.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawAuthenticationService.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawRegistrationService.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AssertionSuperGluuController.java create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java rename {jans-fido2/model/src/main/java/io/jans/fido2/model/entry => jans-orm/model/src/main/java/io/jans/orm/model/fido2}/Fido2AuthenticationData.java (81%) rename {jans-fido2/model/src/main/java/io/jans/fido2/model/entry => jans-orm/model/src/main/java/io/jans/orm/model/fido2}/Fido2AuthenticationEntry.java (83%) rename {jans-fido2/model/src/main/java/io/jans/fido2/model/entry => jans-orm/model/src/main/java/io/jans/orm/model/fido2}/Fido2AuthenticationStatus.java (97%) create mode 100644 jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2DeviceData.java rename {jans-fido2/model/src/main/java/io/jans/fido2/ctap => jans-orm/model/src/main/java/io/jans/orm/model/fido2}/UserVerification.java (88%) diff --git a/docs/script-catalog/person_authentication/super-gluu-external-authenticator/SuperGluuExternalAuthenticator.py b/docs/script-catalog/person_authentication/super-gluu-external-authenticator/SuperGluuExternalAuthenticator.py index 83098109875..84c9badd1da 100644 --- a/docs/script-catalog/person_authentication/super-gluu-external-authenticator/SuperGluuExternalAuthenticator.py +++ b/docs/script-catalog/person_authentication/super-gluu-external-authenticator/SuperGluuExternalAuthenticator.py @@ -5,7 +5,6 @@ # from com.google.android.gcm.server import Sender, Message -from com.notnoop.apns import APNS from java.util import Arrays from org.apache.http.params import CoreConnectionPNames from io.jans.service.cdi.util import CdiUtil @@ -14,7 +13,7 @@ from io.jans.as.server.model.config import ConfigurationFactory from io.jans.as.server.service import AuthenticationService from io.jans.as.server.service import SessionIdService -from io.jans.as.server.service.fido.u2f import DeviceRegistrationService +from io.jans.as.common.service.common.fido2 import RegistrationPersistenceService from io.jans.as.server.service.net import HttpService from io.jans.as.server.util import ServerUtil from io.jans.util import StringHelper @@ -23,10 +22,15 @@ from io.jans.service import MailService from io.jans.as.server.service.push.sns import PushPlatform from io.jans.as.server.service.push.sns import PushSnsService -from io.jans.notify.client import NotifyClientFactory +from io.jans.notify.client import NotifyClientFactory from java.util import Arrays, HashMap, IdentityHashMap, Date from java.time import ZonedDateTime from java.time.format import DateTimeFormatter +from io.jans.as.model.configuration import AppConfiguration +import datetime +import urllib +import sys +import json import datetime import urllib @@ -34,6 +38,14 @@ import sys import json +try: + from com.notnoop.apns import APNS + has_apns = True +except ImportError: + print "Super-Gluu. Load. Native APNS will be disabled. There are missing libs needed to enable it" + has_apns = False + + class PersonAuthentication(PersonAuthenticationType): def __init__(self, currentTimeMillis): self.currentTimeMillis = currentTimeMillis @@ -57,14 +69,14 @@ def init(self, customScript, configurationAttributes): if StringHelper.isEmpty(authentication_mode): print "Super-Gluu. Initialization. Failed to determine authentication_mode. authentication_mode configuration parameter is empty" return False - + self.oneStep = StringHelper.equalsIgnoreCase(authentication_mode, "one_step") self.twoStep = StringHelper.equalsIgnoreCase(authentication_mode, "two_step") if not (self.oneStep or self.twoStep): print "Super-Gluu. Initialization. Valid authentication_mode values are one_step and two_step" return False - + self.enabledPushNotifications = self.initPushNotificationService(configurationAttributes) self.androidUrl = None @@ -101,7 +113,7 @@ def init(self, customScript, configurationAttributes): self.use_audit_group = True print "Super-Gluu. Initialization. Using audit group: %s" % self.audit_group - + if self.use_super_gluu_group or self.use_audit_group: if not configurationAttributes.containsKey("audit_attribute"): print "Super-Gluu. Initialization. Property audit_attribute is not specified" @@ -111,7 +123,7 @@ def init(self, customScript, configurationAttributes): print "Super-Gluu. Initialized successfully. oneStep: '%s', twoStep: '%s', pushNotifications: '%s', customLabel: '%s'" % (self.oneStep, self.twoStep, self.enabledPushNotifications, self.customLabel) - return True + return True def destroy(self, configurationAttributes): print "Super-Gluu. Destroy" @@ -124,10 +136,10 @@ def destroy(self, configurationAttributes): def getApiVersion(self): return 11 - + def getAuthenticationMethodClaims(self, requestParameters): return None - + def isValidAuthenticationMethod(self, usageType, configurationAttributes): return True @@ -158,13 +170,13 @@ def authenticate(self, configurationAttributes, requestParameters, step): return False if form_auth_result in ['timeout']: - if ((step == 1) and self.oneStep) or ((step == 2) and self.twoStep): + if ((step == 1) and self.oneStep) or ((step == 2) and self.twoStep): print "Super-Gluu. Authenticate for step %s. Reinitializing current step" % step identity.setWorkingParameter("retry_current_step", True) return False userService = CdiUtil.bean(UserService) - deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + registrationPersistenceService = CdiUtil.bean(RegistrationPersistenceService) if step == 1: print "Super-Gluu. Authenticate for step 1" @@ -181,11 +193,11 @@ def authenticate(self, configurationAttributes, requestParameters, step): print "Super-Gluu. Authenticate for step 1. User successfully authenticated with u2f_device '%s'" % u2f_device_id else: return False - + if not session_device_status['one_step']: print "Super-Gluu. Authenticate for step 1. u2f_device '%s' is not one step device" % u2f_device_id return False - + # There are two steps only in enrollment mode if session_device_status['enroll']: return validation_result @@ -194,18 +206,20 @@ def authenticate(self, configurationAttributes, requestParameters, step): user_inum = session_device_status['user_inum'] - u2f_device = deviceRegistrationService.findUserDeviceRegistration(user_inum, u2f_device_id, "jansId") + u2f_device = registrationPersistenceService.findRegisteredUserDevice(user_inum, u2f_device_id, "jansId") if u2f_device == None: print "Super-Gluu. Authenticate for step 1. Failed to load u2f_device '%s'" % u2f_device_id return False + found = userService.getUserByInum(user_inum) + user_name = found.getUserId() logged_in = authenticationService.authenticate(user_name) if not logged_in: print "Super-Gluu. Authenticate for step 1. Failed to authenticate user '%s'" % user_name return False print "Super-Gluu. Authenticate for step 1. User '%s' successfully authenticated with u2f_device '%s'" % (user_name, u2f_device_id) - + return True elif self.twoStep: authenticated_user = self.processBasicAuthentication(credentials) @@ -222,26 +236,25 @@ def authenticate(self, configurationAttributes, requestParameters, step): if self.use_audit_group: self.processAuditGroup(authenticated_user, self.audit_attribute, self.audit_group) super_gluu_count_login_steps = 1 - + identity.setWorkingParameter("super_gluu_count_login_steps", super_gluu_count_login_steps) - + if super_gluu_count_login_steps == 1: return True - + auth_method = 'authenticate' enrollment_mode = ServerUtil.getFirstValue(requestParameters, "loginForm:registerButton") if StringHelper.isNotEmpty(enrollment_mode): auth_method = 'enroll' - + if auth_method == 'authenticate': - user_inum = userService.getUserInum(authenticated_user) - u2f_devices_list = deviceRegistrationService.findUserDeviceRegistrations(user_inum, client_redirect_uri, "jansId") + u2f_devices_list = registrationPersistenceService.findByRpRegisteredUserDevices(authenticated_user.getUserId(), client_redirect_uri, "jansId") if u2f_devices_list.size() == 0: auth_method = 'enroll' print "Super-Gluu. Authenticate for step 1. There is no U2F '%s' user devices associated with application '%s'. Changing auth_method to '%s'" % (user_name, client_redirect_uri, auth_method) - + print "Super-Gluu. Authenticate for step 1. auth_method: '%s'" % auth_method - + identity.setWorkingParameter("super_gluu_auth_method", auth_method) return True @@ -249,46 +262,59 @@ def authenticate(self, configurationAttributes, requestParameters, step): return False elif step == 2: print "Super-Gluu. Authenticate for step 2" - - user = authenticationService.getAuthenticatedUser() - if (user == None): - print "Super-Gluu. Authenticate for step 2. Failed to determine user name" - return False - user_name = user.getUserId() - session_attributes = identity.getSessionId().getSessionAttributes() - session_device_status = self.getSessionDeviceStatus(session_attributes, user_name) - if session_device_status == None: - return False - - u2f_device_id = session_device_status['device_id'] - # There are two steps only in enrollment mode - if self.oneStep and session_device_status['enroll']: + if self.oneStep : authenticated_user = self.processBasicAuthentication(credentials) if authenticated_user == None: return False user_inum = userService.getUserInum(authenticated_user) - - attach_result = deviceRegistrationService.attachUserDeviceRegistration(user_inum, u2f_device_id) + session_device_status = self.getSessionDeviceStatus(session_attributes, user_inum) + + if session_device_status['enroll']: + if session_device_status == None: + print "Super-Gluu. oneStep, authenticate for step2, session_device_status is false" + return False - print "Super-Gluu. Authenticate for step 2. Result after attaching u2f_device '%s' to user '%s': '%s'" % (u2f_device_id, user_name, attach_result) + u2f_device_dn = session_device_status['device_dn'] + device_id = session_device_status['device_id'] - return attach_result + attach_result = registrationPersistenceService.attachDeviceRegistrationToUser(user_inum, u2f_device_dn) + + print "Super-Gluu. Authenticate for step 2. Result after attaching u2f_device '%s' to user '%s': '%s'" % (device_id, user_inum, attach_result) + + return attach_result + else: + print "Super-Gluu. one_step but session_device_status['enroll'] = false" + return False elif self.twoStep: - if user_name == None: + user = authenticationService.getAuthenticatedUser() + if (user == None): print "Super-Gluu. Authenticate for step 2. Failed to determine user name" return False + user_name = user.getUserId() + if user_name == None: + print "Super-Gluu. Authenticate for step 2. Failed to determine user id" + return False + + session_device_status = self.getSessionDeviceStatus(session_attributes, user_name) + if session_device_status == None: + print "Super-Gluu. twoStep, authenticate for step2, session_device_status is false" + return False + + u2f_device_id = session_device_status['device_id'] + validation_result = self.validateSessionDeviceStatus(client_redirect_uri, session_device_status, user_name) if validation_result: print "Super-Gluu. Authenticate for step 2. User '%s' successfully authenticated with u2f_device '%s'" % (user_name, u2f_device_id) else: return False - super_gluu_request = json.loads(session_device_status['super_gluu_request']) + print "super_gluu_request %s " % super_gluu_request + auth_method = super_gluu_request['method'] if auth_method in ['enroll', 'authenticate']: if validation_result and self.use_audit_group: @@ -321,8 +347,8 @@ def prepareForStep(self, configurationAttributes, requestParameters, step): if session == None: print "Super-Gluu. Prepare for step 2. Failed to determine session_id" return False + issuer = CdiUtil.bean(AppConfiguration).getIssuer() - issuer = CdiUtil.bean(ConfigurationFactory).getConfiguration().getIssuer() super_gluu_request_dictionary = {'app': client_redirect_uri, 'issuer': issuer, 'state': session.getId(), @@ -332,7 +358,7 @@ def prepareForStep(self, configurationAttributes, requestParameters, step): super_gluu_request = json.dumps(super_gluu_request_dictionary, separators=(',',':')) print "Super-Gluu. Prepare for step 1. Prepared super_gluu_request:", super_gluu_request - + identity.setWorkingParameter("super_gluu_request", super_gluu_request) elif self.twoStep: identity.setWorkingParameter("display_register_action", True) @@ -354,7 +380,7 @@ def prepareForStep(self, configurationAttributes, requestParameters, step): if not StringHelper.equalsIgnoreCase(super_gluu_request, "timeout"): print "Super-Gluu. Prepare for step 2. Request was generated already" return True - + session = CdiUtil.bean(SessionIdService).getSessionId() if session == None: print "Super-Gluu. Prepare for step 2. Failed to determine session_id" @@ -366,8 +392,8 @@ def prepareForStep(self, configurationAttributes, requestParameters, step): return False print "Super-Gluu. Prepare for step 2. auth_method: '%s'" % auth_method - - issuer = CdiUtil.bean(ConfigurationFactory).getAppConfiguration().getIssuer() + + issuer = CdiUtil.bean(AppConfiguration).getIssuer() super_gluu_request_dictionary = {'username': user.getUserId(), 'app': client_redirect_uri, 'issuer': issuer, @@ -407,13 +433,13 @@ def getNextStep(self, configurationAttributes, requestParameters, step): def getExtraParametersForStep(self, configurationAttributes, step): if step == 1: - if self.oneStep: + if self.oneStep: return Arrays.asList("super_gluu_request") elif self.twoStep: return Arrays.asList("display_register_action") elif step == 2: return Arrays.asList("super_gluu_auth_method", "super_gluu_request") - + return None def getCountAuthenticationSteps(self, configurationAttributes): @@ -425,7 +451,7 @@ def getCountAuthenticationSteps(self, configurationAttributes): def getPageForStep(self, configurationAttributes, step): if step == 1: - if self.oneStep: + if self.oneStep: return "/auth/super-gluu/login.xhtml" elif step == 2: if self.oneStep: @@ -465,18 +491,19 @@ def processBasicAuthentication(self, credentials): if find_user_by_uid == None: print "Super-Gluu. Process basic authentication. Failed to find user '%s'" % user_name return None - + return find_user_by_uid - def validateSessionDeviceStatus(self, client_redirect_uri, session_device_status, user_name = None): + def validateSessionDeviceStatus(self, application_name, session_device_status, user_name = None): userService = CdiUtil.bean(UserService) - deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + registrationPersistenceService = CdiUtil.bean(RegistrationPersistenceService) u2f_device_id = session_device_status['device_id'] + u2f_device_dn = session_device_status['device_dn'] u2f_device = None if session_device_status['enroll'] and session_device_status['one_step']: - u2f_device = deviceRegistrationService.findOneStepUserDeviceRegistration(u2f_device_id) + u2f_device = registrationPersistenceService.findOneStepUserDeviceRegistration(u2f_device_dn) if u2f_device == None: print "Super-Gluu. Validate session device status. There is no one step u2f_device '%s'" % u2f_device_id return False @@ -486,16 +513,16 @@ def validateSessionDeviceStatus(self, client_redirect_uri, session_device_status if session_device_status['one_step']: user_inum = session_device_status['user_inum'] - - u2f_device = deviceRegistrationService.findUserDeviceRegistration(user_inum, u2f_device_id) + + u2f_device = registrationPersistenceService.findRegisteredUserDevice(user_inum, u2f_device_id) if u2f_device == None: print "Super-Gluu. Validate session device status. There is no u2f_device '%s' associated with user '%s'" % (u2f_device_id, user_inum) return False - if not StringHelper.equalsIgnoreCase(client_redirect_uri, u2f_device.application): - print "Super-Gluu. Validate session device status. u2f_device '%s' associated with other application '%s'" % (u2f_device_id, u2f_device.application) + if not StringHelper.equalsIgnoreCase(application_name, u2f_device.rpId): + print "Super-Gluu. Validate session device status. u2f_device '%s' associated with other application '%s'" % (u2f_device_id, u2f_device.rpId) return False - + return True def getSessionDeviceStatus(self, session_attributes, user_name): @@ -516,30 +543,36 @@ def getSessionDeviceStatus(self, session_attributes, user_name): return None # Try to find device_id in session attribute - if not session_attributes.containsKey("oxpush2_u2f_device_id"): + if not session_attributes.containsKey("super_gluu_u2f_device_id"): + print "Super-Gluu. Get session device status. There is no u2f_device associated with this request" + return None + + # Try to find device_dn in session attribute + if not session_attributes.containsKey("super_gluu_u2f_device_dn"): print "Super-Gluu. Get session device status. There is no u2f_device associated with this request" return None # Try to find user_inum in session attribute - if not session_attributes.containsKey("oxpush2_u2f_device_user_inum"): + if not session_attributes.containsKey("super_gluu_u2f_device_user_inum"): print "Super-Gluu. Get session device status. There is no user_inum associated with this request" return None - + enroll = False - if session_attributes.containsKey("oxpush2_u2f_device_enroll"): - enroll = StringHelper.equalsIgnoreCase("true", session_attributes.get("oxpush2_u2f_device_enroll")) + if session_attributes.containsKey("super_gluu_u2f_device_enroll"): + enroll = StringHelper.equalsIgnoreCase("true", session_attributes.get("super_gluu_u2f_device_enroll")) one_step = False - if session_attributes.containsKey("oxpush2_u2f_device_one_step"): - one_step = StringHelper.equalsIgnoreCase("true", session_attributes.get("oxpush2_u2f_device_one_step")) - + if session_attributes.containsKey("super_gluu_u2f_device_one_step"): + one_step = StringHelper.equalsIgnoreCase("true", session_attributes.get("super_gluu_u2f_device_one_step")) + super_gluu_request = session_attributes.get("super_gluu_request") - u2f_device_id = session_attributes.get("oxpush2_u2f_device_id") - user_inum = session_attributes.get("oxpush2_u2f_device_user_inum") + u2f_device_dn = session_attributes.get("super_gluu_u2f_device_dn") + u2f_device_id = session_attributes.get("super_gluu_u2f_device_id") + user_inum = session_attributes.get("super_gluu_u2f_device_user_inum") - session_device_status = {"super_gluu_request": super_gluu_request, "device_id": u2f_device_id, "user_inum" : user_inum, "enroll" : enroll, "one_step" : one_step} + session_device_status = {"super_gluu_request": super_gluu_request, "device_id": u2f_device_id, "device_dn": u2f_device_dn, "user_inum" : user_inum, "enroll" : enroll, "one_step" : one_step} print "Super-Gluu. Get session device status. session_device_status: '%s'" % (session_device_status) - + return session_device_status def initPushNotificationService(self, configurationAttributes): @@ -558,25 +591,25 @@ def initPushNotificationService(self, configurationAttributes): def initNativePushNotificationService(self, configurationAttributes): print "Super-Gluu. Initialize native notification services" - + creds = self.loadPushNotificationCreds(configurationAttributes) if creds == None: return False - + try: android_creds = creds["android"]["gcm"] ios_creds = creds["ios"]["apns"] except: print "Super-Gluu. Initialize native notification services. Invalid credentials file format" return False - + self.pushAndroidService = None self.pushAppleService = None if android_creds["enabled"]: - self.pushAndroidService = Sender(android_creds["api_key"]) + self.pushAndroidService = Sender(android_creds["api_key"]) print "Super-Gluu. Initialize native notification services. Created Android notification service" - - if ios_creds["enabled"]: + + if has_apns and ios_creds["enabled"]: p12_file_path = ios_creds["p12_file_path"] p12_password = ios_creds["p12_password"] @@ -608,7 +641,7 @@ def initSnsPushNotificationService(self, configurationAttributes): creds = self.loadPushNotificationCreds(configurationAttributes) if creds == None: return False - + try: sns_creds = creds["sns"] android_creds = creds["android"]["sns"] @@ -616,7 +649,7 @@ def initSnsPushNotificationService(self, configurationAttributes): except: print "Super-Gluu. Initialize SNS notification services. Invalid credentials file format" return False - + self.pushAndroidService = None self.pushAppleService = None if not (android_creds["enabled"] or ios_creds["enabled"]): @@ -634,7 +667,7 @@ def initSnsPushNotificationService(self, configurationAttributes): except: # Ignore exception. Password is not encrypted print "Super-Gluu. Initialize SNS notification services. Assuming that 'sns_secret_access_key' in not encrypted" - + pushSnsService = CdiUtil.bean(PushSnsService) pushClient = pushSnsService.createSnsClient(sns_access_key, sns_secret_access_key, sns_region) @@ -644,7 +677,7 @@ def initSnsPushNotificationService(self, configurationAttributes): print "Super-Gluu. Initialize SNS notification services. Created Android notification service" if ios_creds["enabled"]: - self.pushAppleService = pushClient + self.pushAppleService = pushClient self.pushApplePlatformArn = ios_creds["platform_arn"] self.pushAppleServiceProduction = ios_creds["production"] print "Super-Gluu. Initialize SNS notification services. Created iOS notification service" @@ -661,7 +694,7 @@ def initGluuPushNotificationService(self, configurationAttributes): creds = self.loadPushNotificationCreds(configurationAttributes) if creds == None: return False - + try: gluu_conf = creds["gluu"] android_creds = creds["android"]["gluu"] @@ -669,7 +702,7 @@ def initGluuPushNotificationService(self, configurationAttributes): except: print "Super-Gluu. Initialize Gluu notification services. Invalid credentials file format" return False - + self.pushAndroidService = None self.pushAppleService = None if not (android_creds["enabled"] or ios_creds["enabled"]): @@ -691,28 +724,28 @@ def initGluuPushNotificationService(self, configurationAttributes): if android_creds["enabled"]: gluu_access_key = android_creds["access_key"] gluu_secret_access_key = android_creds["secret_access_key"] - + try: gluu_secret_access_key = encryptionService.decrypt(gluu_secret_access_key) except: # Ignore exception. Password is not encrypted print "Super-Gluu. Initialize Gluu notification services. Assuming that 'gluu_secret_access_key' in not encrypted" - - self.pushAndroidService = gluuClient + + self.pushAndroidService = gluuClient self.pushAndroidServiceAuth = notifyClientFactory.getAuthorization(gluu_access_key, gluu_secret_access_key); print "Super-Gluu. Initialize Gluu notification services. Created Android notification service" if ios_creds["enabled"]: gluu_access_key = ios_creds["access_key"] gluu_secret_access_key = ios_creds["secret_access_key"] - + try: gluu_secret_access_key = encryptionService.decrypt(gluu_secret_access_key) except: # Ignore exception. Password is not encrypted print "Super-Gluu. Initialize Gluu notification services. Assuming that 'gluu_secret_access_key' in not encrypted" - - self.pushAppleService = gluuClient + + self.pushAppleService = gluuClient self.pushAppleServiceAuth = notifyClientFactory.getAuthorization(gluu_access_key, gluu_secret_access_key); print "Super-Gluu. Initialize Gluu notification services. Created iOS notification service" @@ -756,13 +789,13 @@ def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request send_notification_result = True userService = CdiUtil.bean(UserService) - deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + registrationPersistenceService = CdiUtil.bean(RegistrationPersistenceService) user_inum = userService.getUserInum(user_name) send_android = 0 send_ios = 0 - u2f_devices_list = deviceRegistrationService.findUserDeviceRegistrations(user_inum, client_redirect_uri, "jansId", "jansDeviceData", "jansDeviceNotificationConf") + u2f_devices_list = registrationPersistenceService.findByRpRegisteredUserDevices(user_inum, client_redirect_uri, "jansId", "jansDeviceData", "jansDeviceNotificationConf") if u2f_devices_list.size() > 0: for u2f_device in u2f_devices_list: device_data = u2f_device.getDeviceData() @@ -781,19 +814,19 @@ def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request print "Super-Gluu. Send push notification. Apple native push notification service is not enabled" else: send_notification = True - + title = "Super Gluu" message = "Confirm your sign in request to: %s" % client_redirect_uri if self.pushSnsMode or self.pushGluuMode: pushSnsService = CdiUtil.bean(PushSnsService) - targetEndpointArn = self.getTargetEndpointArn(deviceRegistrationService, pushSnsService, PushPlatform.APNS, user, u2f_device) + targetEndpointArn = self.getTargetEndpointArn(registrationPersistenceService, pushSnsService, PushPlatform.APNS, user, u2f_device) if targetEndpointArn == None: - return + return send_notification = True - - sns_push_request_dictionary = { "aps": + + sns_push_request_dictionary = { "aps": { "badge": 0, "alert" : {"body": message, "title" : title}, "category": "ACTIONABLE", @@ -803,12 +836,12 @@ def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request "request" : super_gluu_request } push_message = json.dumps(sns_push_request_dictionary, separators=(',',':')) - + if self.pushSnsMode: apple_push_platform = PushPlatform.APNS if not self.pushAppleServiceProduction: apple_push_platform = PushPlatform.APNS_SANDBOX - + send_notification_result = pushSnsService.sendPushMessage(self.pushAppleService, apple_push_platform, targetEndpointArn, push_message, None) if debug: print "Super-Gluu. Send iOS SNS push notification. token: '%s', message: '%s', send_notification_result: '%s', apple_push_platform: '%s'" % (push_token, push_message, send_notification_result, apple_push_platform) @@ -818,13 +851,13 @@ def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request print "Super-Gluu. Send iOS Gluu push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) else: additional_fields = { "request" : super_gluu_request } - + msgBuilder = APNS.newPayload().alertBody(message).alertTitle(title).sound("default") msgBuilder.category('ACTIONABLE').badge(0) msgBuilder.forNewsstand() msgBuilder.customFields(additional_fields) push_message = msgBuilder.build() - + send_notification_result = self.pushAppleService.push(push_token, push_message) if debug: print "Super-Gluu. Send iOS Native push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) @@ -840,21 +873,21 @@ def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request title = "Super-Gluu" if self.pushSnsMode or self.pushGluuMode: pushSnsService = CdiUtil.bean(PushSnsService) - targetEndpointArn = self.getTargetEndpointArn(deviceRegistrationService, pushSnsService, PushPlatform.GCM, user, u2f_device) + targetEndpointArn = self.getTargetEndpointArn(registrationPersistenceService, pushSnsService, PushPlatform.GCM, user, u2f_device) if targetEndpointArn == None: - return + return send_notification = True - + sns_push_request_dictionary = { "collapse_key": "single", "content_available": True, "time_to_live": 60, - "data": + "data": { "message" : super_gluu_request, "title" : title } } push_message = json.dumps(sns_push_request_dictionary, separators=(',',':')) - + if self.pushSnsMode: send_notification_result = pushSnsService.sendPushMessage(self.pushAndroidService, PushPlatform.GCM, targetEndpointArn, push_message, None) if debug: @@ -866,7 +899,7 @@ def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request else: msgBuilder = Message.Builder().addData("message", super_gluu_request).addData("title", title).collapseKey("single").contentAvailable(True) push_message = msgBuilder.build() - + send_notification_result = self.pushAndroidService.send(push_message, push_token, 3) if debug: print "Super-Gluu. Send Android Native push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) @@ -874,9 +907,9 @@ def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request print "Super-Gluu. Send push notification. send_android: '%s', send_ios: '%s'" % (send_android, send_ios) - def getTargetEndpointArn(self, deviceRegistrationService, pushSnsService, platform, user, u2fDevice): + def getTargetEndpointArn(self, registrationPersistenceService, pushSnsService, platform, user, u2fDevice): targetEndpointArn = None - + # Return endpoint ARN if it created already notificationConf = u2fDevice.getDeviceNotificationConf() if StringHelper.isNotEmpty(notificationConf): @@ -886,7 +919,7 @@ def getTargetEndpointArn(self, deviceRegistrationService, pushSnsService, platfo print "Super-Gluu. Get target endpoint ARN. There is already created target endpoint ARN" return targetEndpointArn - # Create endpoint ARN + # Create endpoint ARN pushClient = None pushClientAuth = None platformApplicationArn = None @@ -907,7 +940,7 @@ def getTargetEndpointArn(self, deviceRegistrationService, pushSnsService, platfo deviceData = u2fDevice.getDeviceData() pushToken = deviceData.getPushToken() - + print "Super-Gluu. Get target endpoint ARN. Attempting to create target endpoint ARN for user: '%s'" % user.getUserId() if self.pushSnsMode: targetEndpointArn = pushSnsService.createPlatformArn(pushClient, platformApplicationArn, pushToken, user) @@ -916,25 +949,25 @@ def getTargetEndpointArn(self, deviceRegistrationService, pushSnsService, platfo registerDeviceResponse = pushClient.registerDevice(pushClientAuth, pushToken, customUserData); if registerDeviceResponse != None and registerDeviceResponse.getStatusCode() == 200: targetEndpointArn = registerDeviceResponse.getEndpointArn() - + if StringHelper.isEmpty(targetEndpointArn): - print "Super-Gluu. Failed to get endpoint ARN for user: '%s'" % user.getUserId() - return None + print "Super-Gluu. Failed to get endpoint ARN for user: '%s'" % user.getUserId() + return None print "Super-Gluu. Get target endpoint ARN. Create target endpoint ARN '%s' for user: '%s'" % (targetEndpointArn, user.getUserId()) - + # Store created endpoint ARN in device entry userInum = user.getAttribute("inum") - u2fDeviceUpdate = deviceRegistrationService.findUserDeviceRegistration(userInum, u2fDevice.getId()) + u2fDeviceUpdate = registrationPersistenceService.findByRpRegisteredUserDevices(userInum, u2fDevice.getId()) u2fDeviceUpdate.setDeviceNotificationConf('{"sns_endpoint_arn" : "%s"}' % targetEndpointArn) - deviceRegistrationService.updateDeviceRegistration(userInum, u2fDeviceUpdate) + registrationPersistenceService.update(u2fDeviceUpdate) return targetEndpointArn def getApplicationUri(self, session_attributes): if self.applicationId != None: return self.applicationId - + if not session_attributes.containsKey("redirect_uri"): return None @@ -950,10 +983,10 @@ def setRequestScopedParameters(self, identity, step): if self.IOSUrl != None and step == 1: downloadMap.put("ios", self.IOSUrl) - + if self.customLabel != None: identity.setWorkingParameter("super_gluu_label", self.customLabel) - + identity.setWorkingParameter("download_url", downloadMap) identity.setWorkingParameter("super_gluu_qr_options", self.customQrOptions) @@ -980,7 +1013,7 @@ def determineGeolocationData(self, remote_ip): http_client = httpService.getHttpsClient() http_client_params = http_client.getParams() http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 15 * 1000) - + geolocation_service_url = "http://ip-api.com/json/%s?fields=49177" % remote_ip geolocation_service_headers = { "Accept" : "application/json" } @@ -996,7 +1029,7 @@ def determineGeolocationData(self, remote_ip): print "Super-Gluu. Determine remote location. Get invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) httpService.consume(http_response) return None - + response_bytes = httpService.getResponseContent(http_response) response_string = httpService.convertEntityToString(response_bytes) httpService.consume(http_response) @@ -1006,9 +1039,9 @@ def determineGeolocationData(self, remote_ip): if response_string == None: print "Super-Gluu. Determine remote location. Get empty response from location server" return None - + response = json.loads(response_string) - + if not StringHelper.equalsIgnoreCase(response['status'], "success"): print "Super-Gluu. Determine remote location. Get response with status: '%s'" % response['status'] return None @@ -1031,7 +1064,7 @@ def processAuditGroup(self, user, attribute, group): if (is_member): print "Super-Gluu. Authenticate for processAuditGroup. User '%s' member of audit group" % user.getUserId() print "Super-Gluu. Authenticate for processAuditGroup. Sending e-mail about user '%s' login to %s" % (user.getUserId(), self.audit_email) - + # Send e-mail to administrator user_id = user.getUserId() mailService = CdiUtil.bean(MailService) diff --git a/jans-auth-server/common/src/main/java/io/jans/as/common/service/AttributeService.java b/jans-auth-server/common/src/main/java/io/jans/as/common/service/AttributeService.java index b751fbb56f3..a602990a3ff 100644 --- a/jans-auth-server/common/src/main/java/io/jans/as/common/service/AttributeService.java +++ b/jans-auth-server/common/src/main/java/io/jans/as/common/service/AttributeService.java @@ -27,9 +27,6 @@ */ public abstract class AttributeService extends io.jans.service.AttributeService { - /** - * - */ private static final long serialVersionUID = -990409035168814270L; @Inject diff --git a/jans-auth-server/common/src/main/java/io/jans/as/common/service/common/fido2/RegistrationPersistenceService.java b/jans-auth-server/common/src/main/java/io/jans/as/common/service/common/fido2/RegistrationPersistenceService.java new file mode 100644 index 00000000000..12a5eb7a8d8 --- /dev/null +++ b/jans-auth-server/common/src/main/java/io/jans/as/common/service/common/fido2/RegistrationPersistenceService.java @@ -0,0 +1,200 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.as.common.service.common.fido2; + +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; +import java.util.UUID; + +import org.slf4j.Logger; + +import io.jans.as.common.model.common.User; +import io.jans.as.common.service.common.UserService; +import io.jans.as.model.config.StaticConfiguration; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.model.base.SimpleBranch; +import io.jans.orm.model.fido2.Fido2RegistrationData; +import io.jans.orm.model.fido2.Fido2RegistrationEntry; +import io.jans.orm.model.fido2.Fido2RegistrationStatus; +import io.jans.orm.search.filter.Filter; +import io.jans.util.StringHelper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Every registration is persisted under Person Entry + * @author Yuriy Movchan + * @version May 08, 2020 + */ +@ApplicationScoped +public class RegistrationPersistenceService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + @Inject + private UserService userService; + + @Inject + private StaticConfiguration staticConfiguration; + + public void save(Fido2RegistrationEntry registrationEntry) { + prepareBranch(registrationEntry.getUserInum()); + + persistenceEntryManager.persist(registrationEntry); + } + + public void update(Fido2RegistrationEntry registrationEntry) { + prepareBranch(registrationEntry.getUserInum()); + + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + + Fido2RegistrationData registrationData = registrationEntry.getRegistrationData(); + registrationData.setUpdatedDate(now); + registrationData.setUpdatedBy(registrationData.getUsername()); + + registrationEntry.setRegistrationStatus(registrationData.getStatus()); + + persistenceEntryManager.merge(registrationEntry); + } + + public void addBranch(final String baseDn) { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName("fido2_register"); + branch.setDn(baseDn); + + persistenceEntryManager.persist(branch); + } + + public boolean containsBranch(final String baseDn) { + return persistenceEntryManager.contains(baseDn, SimpleBranch.class); + } + + public String prepareBranch(final String userInum) { + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (!persistenceEntryManager.hasBranchesSupport(baseDn)) { + return baseDn; + } + + // Create Fido2 base branch for registration entries if needed + if (!containsBranch(baseDn)) { + addBranch(baseDn); + } + + return baseDn; + } + + public Fido2RegistrationEntry findRegisteredUserDevice(String userInum, String deviceId, String... returnAttributes) { + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (persistenceEntryManager.hasBranchesSupport(baseDn)) { + if (!containsBranch(baseDn)) { + return null; + } + } + + String deviceDn = getDnForRegistrationEntry(userInum, deviceId); + + return persistenceEntryManager.find(deviceDn, Fido2RegistrationEntry.class, returnAttributes); + } + + public List findByRpRegisteredUserDevices(String userName, String rpId, String ... returnAttributes) { + String userInum = userService.getUserInum(userName); + if (userInum == null) { + return Collections.emptyList(); + } + + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (persistenceEntryManager.hasBranchesSupport(baseDn)) { + if (!containsBranch(baseDn)) { + return Collections.emptyList(); + } + } + + Filter userInumFilter = Filter.createEqualityFilter("personInum", userInum); + Filter registeredFilter = Filter.createEqualityFilter("jansStatus", Fido2RegistrationStatus.registered.getValue()); + Filter appIdFilter = Filter.createEqualityFilter("jansApp", rpId); + Filter filter = Filter.createANDFilter(userInumFilter, registeredFilter, appIdFilter); + + List fido2RegistrationnEntries = persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, filter, returnAttributes); + + return fido2RegistrationnEntries; + } + + + public boolean attachDeviceRegistrationToUser(String userInum, String deviceDn) { + Fido2RegistrationEntry registrationEntry = persistenceEntryManager.find(Fido2RegistrationEntry.class, deviceDn); + if (registrationEntry == null) { + return false; + } + + User user = userService.getUserByInum(userInum, "uid"); + if (user == null) { + return false; + } + + persistenceEntryManager.remove(deviceDn, Fido2RegistrationEntry.class); + + final String id = UUID.randomUUID().toString(); + + String userAttestationDn = getDnForRegistrationEntry(userInum, id); + registrationEntry.setId(id); + registrationEntry.setDn(userAttestationDn); + registrationEntry.setUserInum(userInum); + + Fido2RegistrationData registrationData = registrationEntry.getRegistrationData(); + registrationData.setUsername(user.getUserId()); + registrationEntry.clearExpiration(); + + save(registrationEntry); + + return true; + } + + public Fido2RegistrationEntry findOneStepUserDeviceRegistration(String deviceDn) { + Fido2RegistrationEntry registrationEntry = persistenceEntryManager.find(Fido2RegistrationEntry.class, deviceDn); + + return registrationEntry; + } + + public String getDnForRegistrationEntry(String userInum, String jsId) { + // Build DN string for Fido2 registration entry + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (StringHelper.isEmpty(jsId)) { + return baseDn; + } + return String.format("jansId=%s,%s", jsId, baseDn); + } + + public String getBaseDnForFido2RegistrationEntries(String userInum) { + final String userBaseDn = getDnForUser(userInum); // "ou=fido2_register,inum=1234,ou=people,o=jans" + if (StringHelper.isEmpty(userInum)) { + return userBaseDn; + } + + return String.format("ou=fido2_register,%s", userBaseDn); + } + + public String getDnForUser(String userInum) { + String peopleDn = getBasedPeopleDn(); + if (StringHelper.isEmpty(userInum)) { + return peopleDn; + } + + return String.format("inum=%s,%s", userInum, peopleDn); + } + + public String getBasedPeopleDn() { + return staticConfiguration.getBaseDn().getPeople(); + } + +} diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java index c8d1228c008..cb92190a25f 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java @@ -61,6 +61,10 @@ public class BaseDnConfiguration { private String par; @XmlElement(name = "ssa") private String ssa; + @XmlElement(name = "fido2Attestation") + private String fido2Attestation; + @XmlElement(name = "fido2Assertion") + private String fido2Assertion; public String getStat() { return stat; @@ -213,4 +217,21 @@ public String getSsa() { public void setSsa(String ssa) { this.ssa = ssa; } + + public String getFido2Attestation() { + return fido2Attestation; + } + + public void setFido2Attestation(String fido2Attestation) { + this.fido2Attestation = fido2Attestation; + } + + public String getFido2Assertion() { + return fido2Assertion; + } + + public void setFido2Assertion(String fido2Assertion) { + this.fido2Assertion = fido2Assertion; + } + } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java index 92fd446ff6f..6c477c86331 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java @@ -184,15 +184,12 @@ private static String createProcessedKey(Map.Entry> baseDn) { } private Map> createCleanServiceBaseDns() { - final String u2fBase = staticConfiguration.getBaseDn().getU2fBase(); final Map> cleanServiceBaseDns = Maps.newHashMap(); cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getClients(), Client.class); cleanServiceBaseDns.put(umaPctService.branchBaseDn(), UmaPCT.class); cleanServiceBaseDns.put(umaResourceService.getBaseDnForResource(), UmaResource.class); - cleanServiceBaseDns.put(String.format("ou=registration_requests,%s", u2fBase), RegisterRequestMessageLdap.class); - cleanServiceBaseDns.put(String.format("ou=registered_devices,%s", u2fBase), DeviceRegistration.class); cleanServiceBaseDns.put(metricService.buildDn(null, null, ApplicationType.OX_AUTH), MetricEntry.class); cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getTokens(), TokenEntity.class); cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getAuthorizations(), ClientAuthorization.class); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/SessionIdService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/SessionIdService.java index 5c1951a686e..2106e8c16d6 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/SessionIdService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/SessionIdService.java @@ -74,7 +74,6 @@ @Named public class SessionIdService { - public static final String SESSION_CUSTOM_STATE = "session_custom_state"; private static final int MAX_MERGE_ATTEMPTS = 3; private static final int DEFAULT_LOCAL_CACHE_EXPIRATION = 2; @@ -335,13 +334,21 @@ public SessionId getSessionId() { sessionId = identity.getSessionId().getId(); } + SessionId result = null; if (StringHelper.isNotEmpty(sessionId)) { - return getSessionId(sessionId); + result = getSessionId(sessionId); + if ((result == null) && identity.getSessionId() != null) { + // Here we cover scenario when user were redirected from /device-code to ACR method + // which call this method in prepareForStep for step 1. The cookie in this case is not updated yet. + // hence actual information about session_id only in identity. + sessionId = identity.getSessionId().getId(); + result = getSessionId(sessionId); + } } else { log.trace("Session cookie not exists"); } - return null; + return result; } public Map getSessionAttributes(SessionId sessionId) { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java index 5b840818e27..b4d1b866ed7 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.jans.as.model.common.FeatureFlagType; +import io.jans.as.model.config.Constants; import io.jans.as.model.error.ErrorResponseFactory; import io.jans.as.common.model.session.SessionId; import io.jans.as.server.service.CookieService; @@ -36,6 +37,8 @@ @Path("/") public class CheckSessionStatusRestWebServiceImpl { + public static final String SESSION_CUSTOM_STATE = "session_custom_state"; + @Inject private Logger log; @@ -64,7 +67,7 @@ public Response requestCheckSessionStatus(@Context HttpServletRequest httpReques response.setState(sessionId.getState().getValue()); response.setAuthTime(sessionId.getAuthenticationTime()); - String sessionCustomState = sessionId.getSessionAttributes().get(SessionIdService.SESSION_CUSTOM_STATE); + String sessionCustomState = sessionId.getSessionAttributes().get(SESSION_CUSTOM_STATE); if (StringHelper.isNotEmpty(sessionCustomState)) { response.setCustomState(sessionCustomState); } @@ -73,7 +76,9 @@ public Response requestCheckSessionStatus(@Context HttpServletRequest httpReques String responseJson = ServerUtil.asJson(response); log.debug("Check session status response: '{}'", responseJson); - return Response.ok().type(MediaType.APPLICATION_JSON).entity(responseJson).build(); + return Response.ok().type(MediaType.APPLICATION_JSON).entity(responseJson) + .cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()) + .header(Constants.PRAGMA, Constants.NO_CACHE).build(); } class CheckSessionResponse { diff --git a/jans-auth-server/server/src/main/webapp/js/gluu-auth.js b/jans-auth-server/server/src/main/webapp/js/gluu-auth.js index 834c4d85d99..18dd59d86dd 100644 --- a/jans-auth-server/server/src/main/webapp/js/gluu-auth.js +++ b/jans-auth-server/server/src/main/webapp/js/gluu-auth.js @@ -160,7 +160,7 @@ var gluu_auth = { (function worker() { $.ajax({ - url: '/oxauth/restv1/session_status', + url: '/jans-auth/restv1/session_status', cache: false, timeout: gluu_auth.checker.timeout, success: function(result, status, xhr) { diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/ctap/AttestationFormat.java b/jans-fido2/model/src/main/java/io/jans/fido2/ctap/AttestationFormat.java index 72d83c6cb23..b4732a46816 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/ctap/AttestationFormat.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/ctap/AttestationFormat.java @@ -8,7 +8,8 @@ public enum AttestationFormat { - fido_u2f("fido-u2f"), packed("packed"), tpm("tpm"), android_key("android-key"), android_safetynet("android-safetynet"), none("none"), apple("apple"); + fido_u2f("fido-u2f"), packed("packed"), tpm("tpm"), android_key("android-key"), android_safetynet("android-safetynet"), none("none"), apple("apple"), + fido_u2f_super_gluu("fido-u2f-super-gluu"); private final String fmt; diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/conf/AppConfiguration.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/conf/AppConfiguration.java index e251163df7f..8bc0abda7af 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/conf/AppConfiguration.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/conf/AppConfiguration.java @@ -11,7 +11,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.jans.as.model.configuration.Configuration; - import io.jans.doc.annotation.DocProperty; import jakarta.enterprise.inject.Vetoed; /** @@ -50,6 +49,10 @@ public class AppConfiguration implements Configuration { private boolean metricReporterEnabled = true; @DocProperty(description = "Custom object class list for dynamic person enrolment") private List personCustomObjectClassList; + @DocProperty(description = "Boolean value to enable disable Super Gluu extension") + private boolean superGluuEnabled; + @DocProperty(description = "Boolean value to enable disable old oxAuth U2F enrollments migration") + private boolean oldU2fMigrationEnabled; private Fido2Configuration fido2Configuration; @@ -141,11 +144,11 @@ public void setMetricReporterKeepDataDays(int metricReporterKeepDataDays) { this.metricReporterKeepDataDays = metricReporterKeepDataDays; } - public Boolean getMetricReporterEnabled() { + public boolean getMetricReporterEnabled() { return metricReporterEnabled; } - public void setMetricReporterEnabled(Boolean metricReporterEnabled) { + public void setMetricReporterEnabled(boolean metricReporterEnabled) { this.metricReporterEnabled = metricReporterEnabled; } @@ -165,4 +168,20 @@ public void setFido2Configuration(Fido2Configuration fido2Configuration) { this.fido2Configuration = fido2Configuration; } + public boolean isSuperGluuEnabled() { + return superGluuEnabled; + } + + public void setSuperGluuEnabled(boolean superGluuEnabled) { + this.superGluuEnabled = superGluuEnabled; + } + + public boolean isOldU2fMigrationEnabled() { + return oldU2fMigrationEnabled; + } + + public void setOldU2fMigrationEnabled(boolean oldU2fMigrationEnabled) { + this.oldU2fMigrationEnabled = oldU2fMigrationEnabled; + } + } diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/sg/SuperGluuMode.java b/jans-fido2/model/src/main/java/io/jans/fido2/sg/SuperGluuMode.java new file mode 100644 index 00000000000..3a84021cfb5 --- /dev/null +++ b/jans-fido2/model/src/main/java/io/jans/fido2/sg/SuperGluuMode.java @@ -0,0 +1,43 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.sg; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Movchan + * @version January 24, 2023 + */ +public enum SuperGluuMode { + + ONE_STEP("one_step"), + TWO_STEP("two_step"); + + private final String mode; + + private static Map KEY_MAPPINGS = new HashMap<>(); + + static { + for (SuperGluuMode enumType : values()) { + KEY_MAPPINGS.put(enumType.getMode(), enumType); + } + } + + SuperGluuMode(String mode) { + this.mode = mode; + } + + public String getMode() { + return mode; + } + + public static SuperGluuMode fromModeValue(String attachment) { + return KEY_MAPPINGS.get(attachment); + } + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/AuthenticatorDataParser.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/AuthenticatorDataParser.java index b379d641695..07752329a28 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/AuthenticatorDataParser.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/AuthenticatorDataParser.java @@ -41,7 +41,7 @@ * @version March 9, 2020 */ /** - * authData — a raw buffer struct containing user info. + * authData � a raw buffer struct containing user info. * Parser for authData or authenticatorData * */ @@ -156,7 +156,7 @@ private long getCborDataSize(byte[] cosePublicKeyBuffer) { while (!parser.isClosed()) { JsonToken t = parser.nextToken(); if (t.isStructEnd()) { - JsonLocation tocloc = parser.getTokenLocation(); + JsonLocation tocloc = parser.getCurrentLocation(); keySize = tocloc.getByteOffset(); break; } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/CertificateService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/CertificateService.java index 4f422c5237a..7ffb91672c8 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/CertificateService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/CertificateService.java @@ -65,7 +65,7 @@ public X509Certificate getCertificate(InputStream is) { } } - public List getCertificates(List certificatePath) { + public List getCertificates(List certificatePath, boolean checkValidaty) { final CertificateFactory cf; try { cf = CertificateFactory.getInstance("X.509"); @@ -80,17 +80,24 @@ public List getCertificates(List certificatePath) { throw new Fido2RuntimeException(e.getMessage(), e); } }).filter(c -> { - try { - c.checkValidity(); - return true; - } catch (CertificateException e) { - log.warn("Certificate not valid {}", c.getIssuerDN().getName()); - throw new Fido2RuntimeException("Certificate not valid", e); - } + if (checkValidaty) { + try { + c.checkValidity(); + return true; + } catch (CertificateException e) { + log.warn("Certificate not valid {}", c.getIssuerDN().getName()); + throw new Fido2RuntimeException("Certificate not valid", e); + } + } + return true; }).collect(Collectors.toList()); } + public List getCertificates(List certificatePath) { + return getCertificates(certificatePath, true); + } + public Map getCertificatesMap(String rootCertificatePath) { List certificates = getCertificates(rootCertificatePath); diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/ChallengeGenerator.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/ChallengeGenerator.java index da56677137c..01b97d44c7a 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/ChallengeGenerator.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/ChallengeGenerator.java @@ -18,6 +18,7 @@ package io.jans.fido2.service; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import jakarta.enterprise.context.ApplicationScoped; @@ -39,4 +40,14 @@ public String getChallenge() { return base64Service.urlEncodeToStringWithoutPadding(buffer); } + public int getChallengeHashCode(String challenge) { + int hash = 0; + byte[] challengeBytes = challenge.getBytes(StandardCharsets.UTF_8); + for (int j = 0; j < challengeBytes.length; j++) { + hash += challengeBytes[j]*j; + } + + return hash; + } + } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/DataMapperService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/DataMapperService.java index aa7df988bb2..c70e9912200 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/DataMapperService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/DataMapperService.java @@ -9,18 +9,23 @@ import java.io.BufferedReader; import java.io.IOException; -import jakarta.annotation.PostConstruct; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - import org.slf4j.Logger; +import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.cbor.CBORParser; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; /** * Conversions to/from JSON format and to/from CBOR format @@ -34,6 +39,7 @@ public class DataMapperService { private Logger log; private ObjectMapper objectMapper; + private ObjectMapper jaxbObjectMapper; private CBORFactory cborFactory; private ObjectMapper cborObjectMapper; @@ -43,6 +49,7 @@ public void init() { this.objectMapper = new ObjectMapper(); this.cborFactory = new CBORFactory(); this.cborObjectMapper = new ObjectMapper(cborFactory); + this.jaxbObjectMapper = jsonMapperWithWrapRoot(); } public JsonNode readTree(byte[] content) throws IOException { @@ -57,6 +64,10 @@ public JsonNode readTree(BufferedReader reader) throws IOException { return objectMapper.readTree(reader); } + public T readValue(String content, Class clazz) throws IOException { + return jaxbObjectMapper.readValue(content, clazz); + } + public ObjectNode createObjectNode() { return objectMapper.createObjectNode(); } @@ -81,4 +92,21 @@ public T convertValue(Object fromValue, Class toValueType) { return objectMapper.convertValue(fromValue, toValueType); } + private ObjectMapper createJsonMapperWithJaxb() { + final AnnotationIntrospector jaxb = new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()); + final AnnotationIntrospector jackson = new JacksonAnnotationIntrospector(); + + final AnnotationIntrospector pair = AnnotationIntrospector.pair(jackson, jaxb); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.getDeserializationConfig().with(pair); + mapper.getSerializationConfig().with(pair); + + return mapper; + } + + private ObjectMapper jsonMapperWithWrapRoot() { + return createJsonMapperWithJaxb().configure(SerializationFeature.WRAP_ROOT_VALUE, true); + } + } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/DigestService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/DigestService.java new file mode 100644 index 00000000000..e907776d9d7 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/DigestService.java @@ -0,0 +1,33 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Method to calculate digests + * + */ +@ApplicationScoped +public class DigestService { + + public byte[] hashSha256(byte[] bytes) { + try { + return MessageDigest.getInstance("SHA-256").digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public byte[] hashSha256(String str) { + return hashSha256(str.getBytes()); + } + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AssertionService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AssertionService.java index da6ad8fcd13..4c1fa7aac6f 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AssertionService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AssertionService.java @@ -6,44 +6,48 @@ package io.jans.fido2.service.operation; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -import io.jans.orm.model.fido2.Fido2RegistrationData; -import io.jans.orm.model.fido2.Fido2RegistrationEntry; -import io.jans.orm.model.fido2.Fido2RegistrationStatus; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.jans.entry.DeviceRegistration; import io.jans.fido2.ctap.AttestationFormat; import io.jans.fido2.ctap.AuthenticatorAttachment; -import io.jans.fido2.ctap.UserVerification; import io.jans.fido2.exception.Fido2CompromisedDevice; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.auth.PublicKeyCredentialDescriptor; import io.jans.fido2.model.conf.AppConfiguration; -import io.jans.fido2.model.entry.Fido2AuthenticationData; -import io.jans.fido2.model.entry.Fido2AuthenticationEntry; -import io.jans.fido2.model.entry.Fido2AuthenticationStatus; +import io.jans.fido2.service.Base64Service; import io.jans.fido2.service.ChallengeGenerator; import io.jans.fido2.service.DataMapperService; import io.jans.fido2.service.persist.AuthenticationPersistenceService; import io.jans.fido2.service.persist.RegistrationPersistenceService; +import io.jans.fido2.service.persist.UserSessionIdService; import io.jans.fido2.service.verifier.AssertionVerifier; import io.jans.fido2.service.verifier.CommonVerifiers; import io.jans.fido2.service.verifier.DomainVerifier; +import io.jans.orm.model.fido2.Fido2AuthenticationData; +import io.jans.orm.model.fido2.Fido2AuthenticationEntry; +import io.jans.orm.model.fido2.Fido2AuthenticationStatus; +import io.jans.orm.model.fido2.Fido2RegistrationData; +import io.jans.orm.model.fido2.Fido2RegistrationEntry; +import io.jans.orm.model.fido2.Fido2RegistrationStatus; +import io.jans.orm.model.fido2.UserVerification; import io.jans.service.net.NetworkService; import io.jans.u2f.service.persist.DeviceRegistrationService; import io.jans.util.StringHelper; -import org.slf4j.Logger; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; /** * Core offering by the FIDO2 server, assertion is invoked upon authentication @@ -72,6 +76,9 @@ public class AssertionService { @Inject private DeviceRegistrationService deviceRegistrationService; + + @Inject + private UserSessionIdService userSessionIdService; @Inject private AssertionVerifier assertionVerifier; @@ -88,18 +95,27 @@ public class AssertionService { @Inject private NetworkService networkService; + @Inject + private Base64Service base64Service; + /* * Requires mandatory parameters: username Support non mandatory parameters: * userVerification, documentDomain, extensions, timeout */ - public JsonNode options(JsonNode params) { + public ObjectNode options(JsonNode params) { log.debug("Assertion options {}", params); + boolean superGluu = commonVerifiers.hasSuperGluu(params); + boolean oneStep = commonVerifiers.isSuperGluuOneStepMode(params); + // Verify request parameters - commonVerifiers.verifyAssertionOptions(params); + String username = null; + if (!(superGluu && oneStep)) { + commonVerifiers.verifyAssertionOptions(params); - // Get username - String username = commonVerifiers.verifyThatFieldString(params, "username"); + // Get username + username = commonVerifiers.verifyThatFieldString(params, "username"); + } // Create result object ObjectNode optionsResponseNode = dataMapperService.createObjectNode(); @@ -118,14 +134,23 @@ public JsonNode options(JsonNode params) { log.debug("Put rpId {}", documentDomain); optionsResponseNode.put("rpId", documentDomain); - // TODO: Verify documentDomain + String applicationId = documentDomain; + if (superGluu && params.hasNonNull(CommonVerifiers.SUPER_GLUU_APP_ID)) { + applicationId = params.get(CommonVerifiers.SUPER_GLUU_APP_ID).asText(); + } + + String requestedKeyHandle = null; + if (superGluu && params.hasNonNull(CommonVerifiers.SUPER_GLUU_KEY_HANDLE)) { + requestedKeyHandle = params.get(CommonVerifiers.SUPER_GLUU_KEY_HANDLE).asText(); + } // Put allowCredentials - Pair allowedCredentialsPair = prepareAllowedCredentials(documentDomain, username); + Pair allowedCredentialsPair = prepareAllowedCredentials(applicationId, username, requestedKeyHandle, superGluu); ArrayNode allowedCredentials = allowedCredentialsPair.getLeft(); if (allowedCredentials.isEmpty()) { throw new Fido2RuntimeException("Can't find associated key(s). Username: " + username); } + optionsResponseNode.set("allowCredentials", allowedCredentials); log.debug("Put allowedCredentials {}", allowedCredentials); @@ -162,19 +187,28 @@ public JsonNode options(JsonNode params) { entity.setDomain(documentDomain); entity.setUserVerificationOption(userVerification); entity.setStatus(Fido2AuthenticationStatus.pending); + if (params.hasNonNull(CommonVerifiers.SUPER_GLUU_APP_ID)) { + entity.setApplicationId(params.get(CommonVerifiers.SUPER_GLUU_APP_ID).asText()); + } // Store original request entity.setAssertionRequest(params.toString()); - authenticationPersistenceService.save(entity); + Fido2AuthenticationEntry authenticationEntity = authenticationPersistenceService.buildFido2AuthenticationEntry(entity, oneStep); + if (params.hasNonNull("session_id")) { + authenticationEntity.setSessionStateId(params.get("session_id").asText()); + } + + authenticationPersistenceService.save(authenticationEntity); return optionsResponseNode; - } - public JsonNode verify(JsonNode params) { + public ObjectNode verify(JsonNode params) { log.debug("authenticateResponse {}", params); + boolean oneStep = commonVerifiers.isSuperGluuOneStepMode(params); + // Verify if there are mandatory request parameters commonVerifiers.verifyBasicPayload(params); commonVerifiers.verifyAssertionType(params, "type"); @@ -199,7 +233,7 @@ public JsonNode verify(JsonNode params) { String challenge = commonVerifiers.getChallenge(clientDataJSONNode); // Find authentication entry - Fido2AuthenticationEntry authenticationEntity = authenticationPersistenceService.findByChallenge(challenge).parallelStream() + Fido2AuthenticationEntry authenticationEntity = authenticationPersistenceService.findByChallenge(challenge, oneStep).parallelStream() .findFirst().orElseThrow(() -> new Fido2RuntimeException( String.format("Can't find associated assertion request by challenge '%s'", challenge))); Fido2AuthenticationData authenticationData = authenticationEntity.getAuthenticationData(); @@ -208,7 +242,7 @@ public JsonNode verify(JsonNode params) { domainVerifier.verifyDomain(authenticationData.getDomain(), clientDataJSONNode); // Find registered public key - Fido2RegistrationEntry registrationEntry = registrationPersistenceService.findByPublicKeyId(keyId) + Fido2RegistrationEntry registrationEntry = registrationPersistenceService.findByPublicKeyId(keyId, authenticationEntity.getRpId()) .orElseThrow(() -> new Fido2RuntimeException(String.format("Couldn't find the key by PublicKeyId '%s'", keyId))); Fido2RegistrationData registrationData = registrationEntry.getRegistrationData(); @@ -236,6 +270,14 @@ public JsonNode verify(JsonNode params) { // initial value in Fido2RegistrationData to minimize DB updates registrationEntry.setCounter(registrationData.getCounter()); registrationPersistenceService.update(registrationEntry); + + // If SessionStateId is not empty update session + String sessionStateId = authenticationEntity.getSessionStateId(); + if (StringHelper.isNotEmpty(sessionStateId)) { + log.debug("There is session id. Setting session id attributes"); + + userSessionIdService.updateUserSessionIdOnFinishRequest(sessionStateId, registrationEntry.getUserInum(), registrationEntry, false, oneStep); + } // Create result object ObjectNode finishResponseNode = dataMapperService.createObjectNode(); @@ -250,34 +292,47 @@ public JsonNode verify(JsonNode params) { return finishResponseNode; } - private Pair prepareAllowedCredentials(String documentDomain, String username) { - // TODO: Add property to enable/disable U2F -> Fido2 migration - List existingFidoRegistrations = deviceRegistrationService.findAllRegisteredByUsername(username, - documentDomain); - if (existingFidoRegistrations.size() > 0) { - deviceRegistrationService.migrateToFido2(existingFidoRegistrations, documentDomain, username); + private Pair prepareAllowedCredentials(String documentDomain, String username, String requestedKeyHandle, boolean superGluu) { + if (appConfiguration.isOldU2fMigrationEnabled()) { + List existingFidoRegistrations = deviceRegistrationService.findAllRegisteredByUsername(username, + documentDomain); + if (existingFidoRegistrations.size() > 0) { + deviceRegistrationService.migrateToFido2(existingFidoRegistrations, documentDomain, username); + } } - List existingFido2Registrations = registrationPersistenceService.findAllRegisteredByUsername(username); + List existingFido2Registrations; + if (superGluu && StringHelper.isNotEmpty(requestedKeyHandle)) { + Fido2RegistrationEntry fido2RegistrationEntry = registrationPersistenceService.findByPublicKeyId(requestedKeyHandle, documentDomain).orElseThrow(() -> new Fido2RuntimeException( + String.format("Can't find associated key '%s' for application '%s'", requestedKeyHandle, documentDomain))); + existingFido2Registrations = Arrays.asList(fido2RegistrationEntry); + } else { + existingFido2Registrations = registrationPersistenceService.findByRpRegisteredUserDevices(username, documentDomain); + } + // f.getRegistrationData().getAttenstationRequest() null check is added to maintain backward compatiblity with U2F devices when U2F devices are migrated to the FIDO2 server List allowedFido2Registrations = existingFido2Registrations.parallelStream() - .filter(f -> StringHelper.equals(documentDomain, f.getRegistrationData().getDomain())) .filter(f -> StringHelper.isNotEmpty(f.getRegistrationData().getPublicKeyId())).collect(Collectors.toList()); - allowedFido2Registrations.forEach((value) -> { - log.debug("attestation request:" + value.getRegistrationData().getAttenstationRequest()); + List allowedFido2Keys = new ArrayList<>(allowedFido2Registrations.size()); + allowedFido2Registrations.forEach((f) -> { + log.debug("attestation request:" + f.getRegistrationData().getAttenstationRequest()); + String transports[]; + if (superGluu) { + transports = new String[] { "net", "qr" }; + } else { + transports = ((f.getRegistrationData().getAttestationType().equalsIgnoreCase(AttestationFormat.apple.getFmt())) || ( f.getRegistrationData().getAttenstationRequest() != null && + f.getRegistrationData().getAttenstationRequest().contains(AuthenticatorAttachment.PLATFORM.getAttachment()))) + + ? new String[] { "internal" } + : new String[] { "usb", "ble", "nfc" }; + } + PublicKeyCredentialDescriptor descriptor = new PublicKeyCredentialDescriptor( + f.getRegistrationData().getType(), transports, f.getRegistrationData().getPublicKeyId()); + + ObjectNode allowedFido2Key = dataMapperService.convertValue(descriptor, ObjectNode.class); + allowedFido2Keys.add(allowedFido2Key); }); - // f.getRegistrationData().getAttenstationRequest() null check is added to maintain backward compatiblity with U2F devices when U2F devices are migrated to the FIDO2 server - List allowedFido2Keys = allowedFido2Registrations.parallelStream() - .map(f -> dataMapperService.convertValue(new PublicKeyCredentialDescriptor(f.getRegistrationData().getType(), - ((f.getRegistrationData().getAttestationType().equalsIgnoreCase(AttestationFormat.apple.getFmt())) || ( f.getRegistrationData().getAttenstationRequest() != null && - f.getRegistrationData().getAttenstationRequest().contains(AuthenticatorAttachment.PLATFORM.getAttachment()))) - - ? new String[] { "internal" } - : new String[] { "usb", "ble", "nfc" }, - f.getRegistrationData().getPublicKeyId()), JsonNode.class)) - .collect(Collectors.toList()); - Optional fidoRegistration = allowedFido2Registrations.parallelStream() .filter(f -> StringUtils.isNotEmpty(f.getRegistrationData().getApplicationId())).findAny(); String applicationId = null; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java index 6e08e192f56..65b61eecc60 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java @@ -6,21 +6,21 @@ package io.jans.fido2.service.operation; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.List; import java.util.stream.Collectors; -import io.jans.orm.model.fido2.Fido2RegistrationData; -import io.jans.orm.model.fido2.Fido2RegistrationEntry; -import io.jans.orm.model.fido2.Fido2RegistrationStatus; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.jans.fido2.ctap.AttestationConveyancePreference; import io.jans.fido2.ctap.AuthenticatorAttachment; import io.jans.fido2.ctap.CoseEC2Algorithm; import io.jans.fido2.ctap.CoseRSAAlgorithm; -import io.jans.fido2.ctap.UserVerification; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.auth.CredAndCounterData; import io.jans.fido2.model.auth.PublicKeyCredentialDescriptor; @@ -30,15 +30,19 @@ import io.jans.fido2.service.ChallengeGenerator; import io.jans.fido2.service.DataMapperService; import io.jans.fido2.service.persist.RegistrationPersistenceService; +import io.jans.fido2.service.persist.UserSessionIdService; import io.jans.fido2.service.verifier.AttestationVerifier; import io.jans.fido2.service.verifier.CommonVerifiers; import io.jans.fido2.service.verifier.DomainVerifier; +import io.jans.fido2.ws.rs.controller.AttestationController; +import io.jans.orm.model.fido2.Fido2DeviceData; +import io.jans.orm.model.fido2.Fido2RegistrationData; +import io.jans.orm.model.fido2.Fido2RegistrationEntry; +import io.jans.orm.model.fido2.Fido2RegistrationStatus; +import io.jans.orm.model.fido2.UserVerification; import io.jans.util.StringHelper; -import org.slf4j.Logger; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; /** * Core offering by the FIDO2 server, attestation is invoked upon enrollment @@ -60,6 +64,9 @@ public class AttestationService { @Inject private AttestationVerifier attestationVerifier; + @Inject + private UserSessionIdService userSessionIdService; + @Inject private DomainVerifier domainVerifier; @@ -80,12 +87,14 @@ public class AttestationService { * mandatory parameters: authenticatorSelection, documentDomain, extensions, * timeout */ - public JsonNode options(JsonNode params) { + public ObjectNode options(JsonNode params) { log.debug("Attestation options {}", params); // Verify request parameters commonVerifiers.verifyAttestationOptions(params); + boolean oneStep = commonVerifiers.isSuperGluuOneStepMode(params); + // Create result object ObjectNode optionsResponseNode = dataMapperService.createObjectNode(); @@ -128,9 +137,11 @@ public JsonNode options(JsonNode params) { log.debug("Put user {}", credentialUserEntityNode); // Put excludeCredentials - ArrayNode excludedCredentials = prepareExcludeCredentials(documentDomain, username); - optionsResponseNode.set("excludeCredentials", excludedCredentials); - log.debug("Put excludeCredentials {}", excludedCredentials); + if (!oneStep) { + ArrayNode excludedCredentials = prepareExcludeCredentials(documentDomain, username); + optionsResponseNode.set("excludeCredentials", excludedCredentials); + log.debug("Put excludeCredentials {}", excludedCredentials); + } // Copy extensions if (params.hasNonNull("extensions")) { @@ -157,20 +168,30 @@ public JsonNode options(JsonNode params) { entity.setChallenge(challenge); entity.setDomain(documentDomain); entity.setStatus(Fido2RegistrationStatus.pending); + if (params.hasNonNull(CommonVerifiers.SUPER_GLUU_APP_ID)) { + entity.setApplicationId(params.get(CommonVerifiers.SUPER_GLUU_APP_ID).asText()); + } // Store original requests entity.setAttenstationRequest(params.toString()); - registrationPersistenceService.save(entity); + Fido2RegistrationEntry registrationEntry = registrationPersistenceService.buildFido2RegistrationEntry(entity, oneStep); + if (params.hasNonNull("session_id")) { + registrationEntry.setSessionStateId(params.get("session_id").asText()); + } + + registrationPersistenceService.save(registrationEntry); log.debug("Saved in LDAP"); return optionsResponseNode; } - public JsonNode verify(JsonNode params) { + public ObjectNode verify(JsonNode params) { log.debug("Attestation verify {}", params); + boolean oneStep = commonVerifiers.isSuperGluuOneStepMode(params); + // Verify if there are mandatory request parameters commonVerifiers.verifyBasicPayload(params); commonVerifiers.verifyAssertionType(params, "type"); @@ -186,7 +207,7 @@ public JsonNode verify(JsonNode params) { String challenge = commonVerifiers.getChallenge(clientDataJSONNode); // Find registration entry - Fido2RegistrationEntry registrationEntry = registrationPersistenceService.findByChallenge(challenge) + Fido2RegistrationEntry registrationEntry = registrationPersistenceService.findByChallenge(challenge, oneStep) .parallelStream().findAny().orElseThrow(() -> new Fido2RuntimeException( String.format("Can't find associated attestatioan request by challenge '%s'", challenge))); Fido2RegistrationData registrationData = registrationEntry.getRegistrationData(); @@ -215,8 +236,42 @@ public JsonNode verify(JsonNode params) { // Fido2RegistrationData to minimize DB updates registrationData.setCounter(registrationEntry.getCounter()); + JsonNode responseDeviceData = responseNode.get("deviceData"); + if (responseDeviceData != null && responseDeviceData.isTextual()) { + try { + Fido2DeviceData deviceData = dataMapperService.readValue( + new String(base64Service.urlDecode(responseDeviceData.asText()), StandardCharsets.UTF_8), + Fido2DeviceData.class); + registrationEntry.setDeviceData(deviceData); + } catch (Exception ex) { + throw new Fido2RuntimeException(String.format("Device data is invalid: %s", responseDeviceData), ex); + } + } + + registrationEntry.setPublicKeyId(registrationData.getPublicKeyId()); + + int publicKeyIdHash = registrationPersistenceService.getPublicKeyIdHash(registrationData.getPublicKeyId()); + registrationEntry.setPublicKeyIdHash(publicKeyIdHash); + + // Get sessionId before cleaning it from registration entry + String sessionStateId = registrationEntry.getSessionStateId(); + registrationEntry.setSessionStateId(null); + + // Set expiration for one_step entry + if (oneStep) { + registrationEntry.setExpiration(); + } else { + registrationEntry.clearExpiration(); + } registrationPersistenceService.update(registrationEntry); + // If sessionStateId is not empty update session + if (StringHelper.isNotEmpty(sessionStateId)) { + log.debug("There is session id. Setting session id attributes"); + + userSessionIdService.updateUserSessionIdOnFinishRequest(sessionStateId, registrationEntry.getUserInum(), registrationEntry, true, oneStep); + } + // Create result object ObjectNode finishResponseNode = dataMapperService.createObjectNode(); @@ -348,7 +403,7 @@ private ObjectNode createRpDomain(String documentDomain) { return null; } - private String generateUserId() { + public String generateUserId() { byte[] buffer = new byte[32]; new SecureRandom().nextBytes(buffer); @@ -366,12 +421,11 @@ private ObjectNode createUserCredentials(String userId, String username, String private ArrayNode prepareExcludeCredentials(String documentDomain, String username) { List existingRegistrations = registrationPersistenceService - .findAllRegisteredByUsername(username); + .findByRpRegisteredUserDevices(username, documentDomain); List excludedKeys = existingRegistrations.parallelStream() - .filter(f -> StringHelper.equals(documentDomain, f.getRegistrationData().getDomain())) .filter(f -> StringHelper.isNotEmpty(f.getRegistrationData().getPublicKeyId())) .map(f -> dataMapperService.convertValue(new PublicKeyCredentialDescriptor( - f.getRegistrationData().getType(), new String[] { "usb", "ble", "nfc", "internal" }, + f.getRegistrationData().getType(), new String[] { "usb", "ble", "nfc", "internal", "net", "qr" }, f.getRegistrationData().getPublicKeyId()), JsonNode.class)) .collect(Collectors.toList()); diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/AuthenticationPersistenceService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/AuthenticationPersistenceService.java index 01af2919887..acf64328320 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/AuthenticationPersistenceService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/AuthenticationPersistenceService.java @@ -13,25 +13,27 @@ import java.util.TimeZone; import java.util.UUID; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import io.jans.as.common.model.common.User; +import io.jans.as.model.config.StaticConfiguration; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.conf.AppConfiguration; -import io.jans.fido2.model.entry.Fido2AuthenticationData; -import io.jans.fido2.model.entry.Fido2AuthenticationEntry; -import io.jans.fido2.model.entry.Fido2AuthenticationStatus; +import io.jans.fido2.service.ChallengeGenerator; import io.jans.fido2.service.shared.UserService; -import io.jans.as.common.model.common.User; -import io.jans.as.model.config.StaticConfiguration; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.model.BatchOperation; import io.jans.orm.model.ProcessBatchOperation; import io.jans.orm.model.SearchScope; import io.jans.orm.model.base.SimpleBranch; +import io.jans.orm.model.fido2.Fido2AuthenticationData; +import io.jans.orm.model.fido2.Fido2AuthenticationEntry; +import io.jans.orm.model.fido2.Fido2AuthenticationStatus; import io.jans.orm.search.filter.Filter; import io.jans.util.StringHelper; -import org.slf4j.Logger; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; /** * Every authentication is persisted under Person Entry @@ -51,6 +53,9 @@ public class AuthenticationPersistenceService { @Inject private AppConfiguration appConfiguration; + @Inject + private ChallengeGenerator challengeGenerator; + @Inject private UserService userService; @@ -58,33 +63,50 @@ public class AuthenticationPersistenceService { private PersistenceEntryManager persistenceEntryManager; public void save(Fido2AuthenticationData authenticationData) { - String userName = authenticationData.getUsername(); - - User user = userService.getUser(userName, "inum"); - if (user == null) { - if (appConfiguration.getFido2Configuration().isUserAutoEnrollment()) { - user = userService.addDefaultUser(userName); - } else { - throw new Fido2RuntimeException("Auto user enrollment was disabled. User not exists!"); - } - } - String userInum = userService.getUserInum(user); + Fido2AuthenticationEntry authenticationEntity = buildFido2AuthenticationEntry(authenticationData, false); - prepareBranch(userInum); + save(authenticationEntity); + } + + public void save(Fido2AuthenticationEntry authenticationEntity) { + prepareBranch(authenticationEntity.getUserInum()); + + persistenceEntryManager.persist(authenticationEntity); + } + + public Fido2AuthenticationEntry buildFido2AuthenticationEntry(Fido2AuthenticationData authenticationData, boolean oneStep) { + String userName = authenticationData.getUsername(); + + String userInum = null; + if (!oneStep) { + User user = userService.getUser(userName, "inum"); + if (user == null) { + if (appConfiguration.getFido2Configuration().isUserAutoEnrollment()) { + user = userService.addDefaultUser(userName); + } else { + throw new Fido2RuntimeException("Auto user enrollment was disabled. User not exists!"); + } + } + userInum = userService.getUserInum(user); + } Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); final String id = UUID.randomUUID().toString(); + final String challenge = authenticationData.getChallenge(); - String dn = getDnForAuthenticationEntry(userInum, id); + String dn = oneStep ? getDnForAuthenticationEntry(null, id) : getDnForAuthenticationEntry(userInum, id); Fido2AuthenticationEntry authenticationEntity = new Fido2AuthenticationEntry(dn, authenticationData.getId(), now, userInum, authenticationData); authenticationEntity.setAuthenticationStatus(authenticationData.getStatus()); + if (StringUtils.isNotEmpty(challenge)) { + authenticationEntity.setChallengeHash(String.valueOf(challengeGenerator.getChallengeHashCode(challenge))); + } + authenticationEntity.setRpId(authenticationData.getApplicationId()); authenticationData.setCreatedDate(now); authenticationData.setCreatedBy(userName); - - persistenceEntryManager.persist(authenticationEntity); - } + return authenticationEntity; + } public void update(Fido2AuthenticationEntry authenticationEntity) { Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); @@ -123,19 +145,27 @@ public void prepareBranch(final String userInum) { } } - public List findByChallenge(String challenge) { - String baseDn = getBaseDnForFido2AuthenticationEntries(null); + public List findByChallenge(String challenge, boolean oneStep) { + String baseDn = oneStep ? getDnForAuthenticationEntry(null, null) : getBaseDnForFido2AuthenticationEntries(null); Filter codeChallengFilter = Filter.createEqualityFilter("jansCodeChallenge", challenge); + Filter codeChallengHashCodeFilter = Filter.createEqualityFilter("jansCodeChallengeHash", String.valueOf(challengeGenerator.getChallengeHashCode(challenge))); + Filter filter = Filter.createANDFilter(codeChallengFilter, codeChallengHashCodeFilter); - List fido2AuthenticationEntries = persistenceEntryManager.findEntries(baseDn, Fido2AuthenticationEntry.class, codeChallengFilter); + List fido2AuthenticationEntries = persistenceEntryManager.findEntries(baseDn, Fido2AuthenticationEntry.class, filter); return fido2AuthenticationEntries; } public String getDnForAuthenticationEntry(String userInum, String jsId) { + String baseDn; + if (StringHelper.isEmpty(userInum)) { + baseDn = staticConfiguration.getBaseDn().getFido2Attestation(); + } else { + // Build DN string for Fido2 registration entry + baseDn = getBaseDnForFido2AuthenticationEntries(userInum); + } // Build DN string for Fido2 authentication entry - String baseDn = getBaseDnForFido2AuthenticationEntries(userInum); if (StringHelper.isEmpty(jsId)) { return baseDn; } @@ -237,4 +267,5 @@ private Filter getEmptyAuthenticationBranchFilter() { Filter.createEqualityFilter("numsubordinates", "0"), Filter.createEqualityFilter("hasSubordinates", "FALSE"))); } + } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/RegistrationPersistenceService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/RegistrationPersistenceService.java index 931a3459189..647b69bf3f0 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/RegistrationPersistenceService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/RegistrationPersistenceService.java @@ -6,6 +6,7 @@ package io.jans.fido2.service.persist; +import java.nio.charset.StandardCharsets; import java.util.Calendar; import java.util.Collections; import java.util.Date; @@ -15,26 +16,26 @@ import java.util.TimeZone; import java.util.UUID; -import io.jans.orm.model.fido2.Fido2RegistrationData; -import io.jans.orm.model.fido2.Fido2RegistrationEntry; -import io.jans.orm.model.fido2.Fido2RegistrationStatus; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; + +import io.jans.as.common.model.common.User; +import io.jans.as.model.config.StaticConfiguration; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.conf.AppConfiguration; import io.jans.fido2.service.shared.UserService; -import io.jans.as.common.model.common.User; -import io.jans.as.model.config.StaticConfiguration; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.model.BatchOperation; import io.jans.orm.model.ProcessBatchOperation; import io.jans.orm.model.SearchScope; import io.jans.orm.model.base.SimpleBranch; +import io.jans.orm.model.fido2.Fido2RegistrationData; +import io.jans.orm.model.fido2.Fido2RegistrationEntry; +import io.jans.orm.model.fido2.Fido2RegistrationStatus; import io.jans.orm.search.filter.Filter; import io.jans.util.StringHelper; -import org.slf4j.Logger; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; /** * Every registration is persisted under Person Entry @@ -42,7 +43,7 @@ * @version May 08, 2020 */ @ApplicationScoped -public class RegistrationPersistenceService { +public class RegistrationPersistenceService extends io.jans.as.common.service.common.fido2.RegistrationPersistenceService { @Inject private Logger log; @@ -60,85 +61,58 @@ public class RegistrationPersistenceService { private PersistenceEntryManager persistenceEntryManager; public void save(Fido2RegistrationData registrationData) { - Fido2RegistrationEntry registrationEntry = buildFido2RegistrationEntry(registrationData); + Fido2RegistrationEntry registrationEntry = buildFido2RegistrationEntry(registrationData, false); - persistenceEntryManager.persist(registrationEntry); + save(registrationEntry); } - public Fido2RegistrationEntry buildFido2RegistrationEntry(Fido2RegistrationData registrationData) { + public Fido2RegistrationEntry buildFido2RegistrationEntry(Fido2RegistrationData registrationData, boolean oneStep) { String userName = registrationData.getUsername(); - - User user = userService.getUser(userName, "inum"); - if (user == null) { - if (appConfiguration.getFido2Configuration().isUserAutoEnrollment()) { - user = userService.addDefaultUser(userName); - } else { - throw new Fido2RuntimeException("Auto user enrollment was disabled. User not exists!"); - } - } - String userInum = userService.getUserInum(user); - prepareBranch(userInum); + String userInum = null; + if (!oneStep) { + User user = userService.getUser(userName, "inum"); + if (user == null) { + if (appConfiguration.getFido2Configuration().isUserAutoEnrollment()) { + user = userService.addDefaultUser(userName); + } else { + throw new Fido2RuntimeException("Auto user enrollment was disabled. User not exists!"); + } + } + userInum = userService.getUserInum(user); + } Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); final String id = UUID.randomUUID().toString(); final String challenge = registrationData.getChallenge(); - String dn = getDnForRegistrationEntry(userInum, id); + String dn = oneStep ? getDnForRegistrationEntry(null, id) : getDnForRegistrationEntry(userInum, id); Fido2RegistrationEntry registrationEntry = new Fido2RegistrationEntry(dn, id, now, userInum, registrationData, challenge); registrationEntry.setRegistrationStatus(registrationData.getStatus()); if (StringUtils.isNotEmpty(challenge)) { - registrationEntry.setChallangeHash(String.valueOf(getChallengeHashCode(challenge))); + registrationEntry.setChallengeHash(String.valueOf(getChallengeHashCode(challenge))); } - + registrationEntry.setRpId(registrationData.getApplicationId()); + registrationData.setCreatedDate(now); registrationData.setCreatedBy(userName); return registrationEntry; } - public void update(Fido2RegistrationEntry registrationEntry) { - Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); - - Fido2RegistrationData registrationData = registrationEntry.getRegistrationData(); - registrationData.setUpdatedDate(now); - registrationData.setUpdatedBy(registrationData.getUsername()); - - registrationEntry.setPublicKeyId(registrationData.getPublicKeyId()); - registrationEntry.setRegistrationStatus(registrationData.getStatus()); - - persistenceEntryManager.merge(registrationEntry); - } - - public void addBranch(final String baseDn) { - SimpleBranch branch = new SimpleBranch(); - branch.setOrganizationalUnitName("fido2_register"); - branch.setDn(baseDn); - - persistenceEntryManager.persist(branch); - } - - public boolean containsBranch(final String baseDn) { - return persistenceEntryManager.contains(baseDn, SimpleBranch.class); - } - - public void prepareBranch(final String userInum) { - String baseDn = getBaseDnForFido2RegistrationEntries(userInum); - if (!persistenceEntryManager.hasBranchesSupport(baseDn)) { - return; - } - - // Create Fido2 base branch for registration entries if needed - if (!containsBranch(baseDn)) { - addBranch(baseDn); - } - } - - public Optional findByPublicKeyId(String publicKeyId) { + public Optional findByPublicKeyId(String publicKeyId, String rpId) { String baseDn = getBaseDnForFido2RegistrationEntries(null); + Filter filter; Filter publicKeyIdFilter = Filter.createEqualityFilter("jansPublicKeyId", publicKeyId); - List fido2RegistrationnEntries = persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, publicKeyIdFilter); + Filter publicKeyIdHashFilter = Filter.createEqualityFilter("jansPublicKeyIdHash", getPublicKeyIdHash(publicKeyId)); + if (StringHelper.isNotEmpty(rpId)) { + Filter appIdFilter = Filter.createEqualityFilter("jansApp", rpId); + filter = Filter.createORFilter(publicKeyIdFilter, publicKeyIdHashFilter, appIdFilter); + } else { + filter = Filter.createORFilter(publicKeyIdFilter, publicKeyIdHashFilter); + } + List fido2RegistrationnEntries = persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, filter); if (fido2RegistrationnEntries.size() > 0) { return Optional.of(fido2RegistrationnEntries.get(0)); @@ -188,9 +162,9 @@ public List findAllRegisteredByUsername(String username) return fido2RegistrationnEntries; } - - public List findByChallenge(String challenge) { - String baseDn = getBaseDnForFido2RegistrationEntries(null); + + public List findByChallenge(String challenge, boolean oneStep) { + String baseDn = oneStep ? getDnForRegistrationEntry(null, null) : getBaseDnForFido2RegistrationEntries(null); Filter codeChallengFilter = Filter.createEqualityFilter("jansCodeChallenge", challenge); Filter codeChallengHashCodeFilter = Filter.createEqualityFilter("jansCodeChallengeHash", String.valueOf(getChallengeHashCode(challenge))); @@ -201,34 +175,10 @@ public List findByChallenge(String challenge) { return fido2RegistrationnEntries; } - public String getDnForRegistrationEntry(String userInum, String jsId) { - // Build DN string for Fido2 registration entry - String baseDn = getBaseDnForFido2RegistrationEntries(userInum); - if (StringHelper.isEmpty(jsId)) { - return baseDn; - } - return String.format("jansId=%s,%s", jsId, baseDn); - } - - public String getBaseDnForFido2RegistrationEntries(String userInum) { - final String userBaseDn = getDnForUser(userInum); // "ou=fido2_register,inum=1234,ou=people,o=jans" - if (StringHelper.isEmpty(userInum)) { - return userBaseDn; - } - - return String.format("ou=fido2_register,%s", userBaseDn); + public String getBasedPeopleDn() { + return staticConfiguration.getBaseDn().getPeople(); } - public String getDnForUser(String userInum) { - String peopleDn = staticConfiguration.getBaseDn().getPeople(); - if (StringHelper.isEmpty(userInum)) { - return peopleDn; - } - - return String.format("inum=%s,%s", userInum, peopleDn); - } - - public void cleanup(Date now, int batchSize) { // Cleaning expired entries BatchOperation cleanerRegistrationBatchService = new ProcessBatchOperation() { @@ -293,7 +243,7 @@ private Filter getEmptyRegistrationBranchFilter() { public int getChallengeHashCode(String challenge) { int hash = 0; - byte[] challengeBytes = challenge.getBytes(); + byte[] challengeBytes = challenge.getBytes(StandardCharsets.UTF_8); for (int j = 0; j < challengeBytes.length; j++) { hash += challengeBytes[j]*j; } @@ -301,4 +251,34 @@ public int getChallengeHashCode(String challenge) { return hash; } + /* + * Generate non unique hash code to split keyHandle among small cluster with 10-20 elements + * + * This hash code will be used to generate small LDAP indexes + */ + public int getPublicKeyIdHash(String publicKeyId) { + byte[] publicKeyIdBytes = publicKeyId.getBytes(StandardCharsets.UTF_8); + int hash = 0; + for (int j = 0; j < publicKeyIdBytes.length; j++) { + hash += publicKeyIdBytes[j]*j; + } + + return hash; + } + + @Override + public String getDnForRegistrationEntry(String userInum, String jsId) { + String baseDn; + if (StringHelper.isEmpty(userInum)) { + baseDn = staticConfiguration.getBaseDn().getFido2Attestation(); + } else { + // Build DN string for Fido2 registration entry + baseDn = getBaseDnForFido2RegistrationEntries(userInum); + } + if (StringHelper.isEmpty(jsId)) { + return baseDn; + } + return String.format("jansId=%s,%s", jsId, baseDn); + } + } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/UserSessionIdService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/UserSessionIdService.java new file mode 100644 index 00000000000..55bc5ae4a00 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/persist/UserSessionIdService.java @@ -0,0 +1,134 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service.persist; + +import java.util.Date; +import java.util.Map; + +import org.slf4j.Logger; + +import io.jans.as.common.model.session.SessionId; +import io.jans.as.common.model.session.SessionIdState; +import io.jans.as.model.config.StaticConfiguration; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.model.fido2.Fido2RegistrationEntry; +import io.jans.orm.model.fido2.Fido2RegistrationStatus; +import io.jans.util.StringHelper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Configure user session to confirm Fido2 device authentication + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@ApplicationScoped +public class UserSessionIdService { + + public static final String AUTHENTICATED_USER = "auth_user"; + + @Inject + private Logger log; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + public boolean isValidSessionId(String sessionId, String userName) { + SessionId session = getSessionId(sessionId); + if (session == null) { + log.error("Specified session_id '{}' is invalid", sessionId); + return false; + } + + if (StringHelper.isNotEmpty(userName)) { + String sessionIdUser = session.getSessionAttributes().get(AUTHENTICATED_USER); + if (!StringHelper.equalsIgnoreCase(userName, sessionIdUser)) { + log.error("Username '{}' and session_id '{}' don't match", userName, sessionId); + return false; + } + } + + return true; + } + + public void updateUserSessionIdOnFinishRequest(String sessionId, String userInum, Fido2RegistrationEntry registrationEntry, boolean enroll, boolean oneStep) { + SessionId entity = getSessionId(sessionId); + if (entity == null) { + return; + } + + Map sessionAttributes = entity.getSessionAttributes(); + if (Fido2RegistrationStatus.registered == registrationEntry.getRegistrationData().getStatus()) { + sessionAttributes.put("session_custom_state", "approved"); + } else { + sessionAttributes.put("session_custom_state", "declined"); + } + sessionAttributes.put("super_gluu_u2f_device_id", registrationEntry.getId()); + sessionAttributes.put("super_gluu_u2f_device_dn", registrationEntry.getDn()); + sessionAttributes.put("super_gluu_u2f_device_user_inum", userInum); + sessionAttributes.put("super_gluu_u2f_device_enroll", Boolean.toString(enroll)); + sessionAttributes.put("super_gluu_u2f_device_one_step", Boolean.toString(oneStep)); + + updateSessionId(entity); + } + + public void updateUserSessionIdOnError(String sessionId) { + SessionId entity = getSessionId(sessionId); + if (entity == null) { + return; + } + + Map sessionAttributes = entity.getSessionAttributes(); + sessionAttributes.put("session_custom_state", "declined"); + + // TODO: Check if this not reset ttl and expiration. Check original SessionId service + updateSessionId(entity); + } + + public void updateSessionId(SessionId entity) { + entity.setLastUsedAt(new Date()); + persistenceEntryManager.merge(entity); + } + + private SessionId getSessionId(String sessionId) { + if (StringHelper.isEmpty(sessionId)) { + return null; + } + + final SessionId entity; + try { + String sessionDn = buildDn(sessionId); + entity = persistenceEntryManager.find(SessionId.class, sessionDn); + if (entity == null) { + log.warn("Failed to load session id '{}'", sessionId); + } + } catch (Exception ex) { + log.trace(ex.getMessage(), ex); + return null; + } + + if (entity == null) { + return null; + } + + if (SessionIdState.UNAUTHENTICATED != entity.getState()) { + log.warn("Unexpected session id '{}' state: '{}'", sessionId, entity.getState()); + return null; + } + + return entity; + } + + private String buildDn(String sessionId) { + return String.format("jansId=%s,%s", sessionId, staticConfiguration.getBaseDn().getSessions()); + } + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/AppleAssertionFormatProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/AppleAssertionFormatProcessor.java index 89428afb496..e19fc32fc7f 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/AppleAssertionFormatProcessor.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/AppleAssertionFormatProcessor.java @@ -15,6 +15,7 @@ import java.security.PublicKey; +import io.jans.orm.model.fido2.Fido2AuthenticationData; import io.jans.orm.model.fido2.Fido2RegistrationData; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -30,7 +31,6 @@ import io.jans.fido2.exception.Fido2CompromisedDevice; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.auth.AuthData; -import io.jans.fido2.model.entry.Fido2AuthenticationData; import io.jans.fido2.service.AuthenticatorDataParser; import io.jans.fido2.service.Base64Service; import io.jans.fido2.service.CoseService; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/PackedAssertionFormatProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/PackedAssertionFormatProcessor.java index 5e46633c682..5783298766b 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/PackedAssertionFormatProcessor.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/PackedAssertionFormatProcessor.java @@ -20,6 +20,7 @@ import java.security.PublicKey; +import io.jans.orm.model.fido2.Fido2AuthenticationData; import io.jans.orm.model.fido2.Fido2RegistrationData; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -31,7 +32,6 @@ import io.jans.fido2.exception.Fido2CompromisedDevice; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.auth.AuthData; -import io.jans.fido2.model.entry.Fido2AuthenticationData; import io.jans.fido2.service.AuthenticatorDataParser; import io.jans.fido2.service.Base64Service; import io.jans.fido2.service.CoseService; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FAssertionFormatProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FAssertionFormatProcessor.java index 0371850d76e..c08c929dbb4 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FAssertionFormatProcessor.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FAssertionFormatProcessor.java @@ -20,6 +20,7 @@ import java.security.PublicKey; +import io.jans.orm.model.fido2.Fido2AuthenticationData; import io.jans.orm.model.fido2.Fido2RegistrationData; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -29,7 +30,6 @@ import io.jans.fido2.ctap.AttestationFormat; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.auth.AuthData; -import io.jans.fido2.model.entry.Fido2AuthenticationData; import io.jans.fido2.service.AuthenticatorDataParser; import io.jans.fido2.service.Base64Service; import io.jans.fido2.service.CoseService; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FSuperGluuAssertionFormatProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FSuperGluuAssertionFormatProcessor.java new file mode 100644 index 00000000000..825233a292b --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/U2FSuperGluuAssertionFormatProcessor.java @@ -0,0 +1,113 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +/* + * Copyright (c) 2018 Mastercard + * + * 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 io.jans.fido2.service.processor.assertion; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; + +import io.jans.orm.model.fido2.Fido2AuthenticationData; +import io.jans.orm.model.fido2.Fido2RegistrationData; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import io.jans.fido2.ctap.AttestationFormat; +import io.jans.fido2.exception.Fido2RuntimeException; +import io.jans.fido2.model.auth.AuthData; +import io.jans.fido2.service.AuthenticatorDataParser; +import io.jans.fido2.service.Base64Service; +import io.jans.fido2.service.CoseService; +import io.jans.fido2.service.DataMapperService; +import io.jans.fido2.service.processors.AssertionFormatProcessor; +import io.jans.fido2.service.verifier.AuthenticatorDataVerifier; +import io.jans.fido2.service.verifier.CommonVerifiers; +import io.jans.fido2.service.verifier.UserVerificationVerifier; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Class which processes assertions of "fido2-u2f" fmt (attestation type) + * + */ +@ApplicationScoped +public class U2FSuperGluuAssertionFormatProcessor implements AssertionFormatProcessor { + + @Inject + private Logger log; + + @Inject + private CoseService coseService; + + @Inject + private CommonVerifiers commonVerifiers; + + @Inject + private AuthenticatorDataVerifier authenticatorDataVerifier; + + @Inject + private UserVerificationVerifier userVerificationVerifier; + + @Inject + private AuthenticatorDataParser authenticatorDataParser; + + @Inject + private DataMapperService dataMapperService; + + @Inject + private Base64Service base64Service; + + @Override + public AttestationFormat getAttestationFormat() { + return AttestationFormat.fido_u2f_super_gluu; + } + + @Override + public void process(String base64AuthenticatorData, String signature, String clientDataJson, Fido2RegistrationData registration, + Fido2AuthenticationData authenticationEntity) { + AuthData authData = authenticatorDataParser.parseAssertionData(base64AuthenticatorData); + +// String clientDataRaw = commonVerifiers.verifyClientRaw(response).asText(); + userVerificationVerifier.verifyUserPresent(authData); + + String clientDataJsonString = new String(base64Service.urlDecode(clientDataJson), StandardCharsets.UTF_8); + // Update to conform Super Gluu + clientDataJsonString = clientDataJsonString.replace("type", "typ").replaceAll("webauthn.get", "navigator.id.getAssertion"); + + byte[] clientDataHash = DigestUtils.getSha256Digest().digest(clientDataJsonString.getBytes(StandardCharsets.UTF_8)); + + try { + int counter = authenticatorDataParser.parseCounter(authData.getCounters()); + commonVerifiers.verifyCounter(registration.getCounter(), counter); + registration.setCounter(counter); + + JsonNode uncompressedECPointNode = dataMapperService.cborReadTree(base64Service.urlDecode(registration.getUncompressedECPoint())); + PublicKey publicKey = coseService.createUncompressedPointFromCOSEPublicKey(uncompressedECPointNode); + log.debug("Uncompressed ECpoint node {}", uncompressedECPointNode.toString()); + log.debug("Public key hex {}", Hex.encodeHexString(publicKey.getEncoded())); + + authenticatorDataVerifier.verifyAssertionSignature(authData, clientDataHash, signature, publicKey, registration.getSignatureAlgorithm()); + } catch (Exception ex) { + throw new Fido2RuntimeException("Failed to check U2F assertion", ex); + } + } +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/U2FSuperGluuAttestationProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/U2FSuperGluuAttestationProcessor.java new file mode 100644 index 00000000000..ec22a2c8a6e --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/U2FSuperGluuAttestationProcessor.java @@ -0,0 +1,132 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +/* + * Copyright (c) 2018 Mastercard + * + * 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 io.jans.fido2.service.processor.attestation; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.jans.orm.model.fido2.Fido2RegistrationData; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.jans.fido2.ctap.AttestationFormat; +import io.jans.fido2.exception.Fido2MissingAttestationCertException; +import io.jans.fido2.model.auth.AuthData; +import io.jans.fido2.model.auth.CredAndCounterData; +import io.jans.fido2.model.conf.AppConfiguration; +import io.jans.fido2.service.Base64Service; +import io.jans.fido2.service.CertificateService; +import io.jans.fido2.service.CoseService; +import io.jans.fido2.service.mds.AttestationCertificateService; +import io.jans.fido2.service.processors.AttestationFormatProcessor; +import io.jans.fido2.service.verifier.AuthenticatorDataVerifier; +import io.jans.fido2.service.verifier.CertificateVerifier; +import io.jans.fido2.service.verifier.CommonVerifiers; +import io.jans.fido2.service.verifier.UserVerificationVerifier; + +import org.apache.commons.codec.digest.DigestUtils; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Attestation processor for attestations of fmt =fido-u2f + * + */ +@ApplicationScoped +public class U2FSuperGluuAttestationProcessor implements AttestationFormatProcessor { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CommonVerifiers commonVerifiers; + + @Inject + private AuthenticatorDataVerifier authenticatorDataVerifier; + + @Inject + private UserVerificationVerifier userVerificationVerifier; + + @Inject + private AttestationCertificateService attestationCertificateService; + + @Inject + private CertificateVerifier certificateVerifier; + + @Inject + private CoseService coseService; + + @Inject + private Base64Service base64Service; + + @Inject + private CertificateService certificateService; + + @Override + public AttestationFormat getAttestationFormat() { + return AttestationFormat.fido_u2f_super_gluu; + } + + @Override + public void process(JsonNode attStmt, AuthData authData, Fido2RegistrationData registration, byte[] clientDataHash, + CredAndCounterData credIdAndCounters) { + int alg = -7; + + String signature = commonVerifiers.verifyBase64String(attStmt.get("sig")); + commonVerifiers.verifyAAGUIDZeroed(authData); + + userVerificationVerifier.verifyUserPresent(authData); + + if (attStmt.hasNonNull("x5c")) { + Iterator i = attStmt.get("x5c").elements(); + ArrayList certificatePath = new ArrayList(); + while (i.hasNext()) { + certificatePath.add(i.next().asText()); + } + // TODO: Regenerate Super Gluu Cert + List certificates = certificateService.getCertificates(certificatePath, false); + + credIdAndCounters.setSignatureAlgorithm(alg); +// List trustAnchorCertificates = attestationCertificateService.getAttestationRootCertificates((JsonNode) null, certificates); +// Certificate verifiedCert = certificateVerifier.verifyAttestationCertificates(certificates, trustAnchorCertificates); + Certificate verifiedCert = certificates.get(0); + byte[] challengeHash = DigestUtils.getSha256Digest().digest(registration.getChallenge().getBytes(Charset.forName("UTF-8"))); + + // RP ID hash is application for Super Gluu + byte[] rpIdhash = DigestUtils.getSha256Digest().digest(registration.getApplicationId().getBytes(Charset.forName("UTF-8"))); + + authenticatorDataVerifier.verifyU2FAttestationSignature(authData, rpIdhash, challengeHash, signature, verifiedCert, alg); + } + + credIdAndCounters.setAttestationType(getAttestationFormat().getFmt()); + credIdAndCounters.setCredId(base64Service.urlEncodeToString(authData.getCredId())); + credIdAndCounters.setUncompressedEcPoint(base64Service.urlEncodeToString(authData.getCosePublicKey())); + } + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processors/AssertionFormatProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processors/AssertionFormatProcessor.java index 94c8a1794f0..1592d5f4836 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/processors/AssertionFormatProcessor.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processors/AssertionFormatProcessor.java @@ -19,7 +19,7 @@ package io.jans.fido2.service.processors; import io.jans.fido2.ctap.AttestationFormat; -import io.jans.fido2.model.entry.Fido2AuthenticationData; +import io.jans.orm.model.fido2.Fido2AuthenticationData; import io.jans.orm.model.fido2.Fido2RegistrationData; /** diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawAuthenticationService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawAuthenticationService.java new file mode 100644 index 00000000000..f642c510ec1 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawAuthenticationService.java @@ -0,0 +1,58 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +/* + * Copyright (c) 2018 Mastercard + * + * 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 io.jans.fido2.service.sg; + +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; + +import io.jans.as.model.fido.u2f.exception.BadInputException; +import io.jans.as.model.fido.u2f.message.RawAuthenticateResponse; +import io.jans.as.model.util.Base64Util; +import io.jans.util.io.ByteDataInputStream; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +/** + * Provides operations with U2F RAW registration response + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +@ApplicationScoped +public class RawAuthenticationService { + + public static final String AUTHENTICATE_GET_TYPE = "navigator.id.getAssertion"; + public static final String AUTHENTICATE_CANCEL_TYPE = "navigator.id.cancelAssertion"; + public static final String[] SUPPORTED_AUTHENTICATE_TYPES = new String[] { AUTHENTICATE_GET_TYPE, AUTHENTICATE_CANCEL_TYPE }; + + @Inject + private Logger log; + + public RawAuthenticateResponse parseRawAuthenticateResponse(String rawDataBase64) { + ByteDataInputStream bis = new ByteDataInputStream(Base64Util.base64urldecode(rawDataBase64)); + try { + return new RawAuthenticateResponse(bis.readSigned(), bis.readInt(), bis.readAll()); + } catch (IOException ex) { + throw new BadInputException("Failed to parse RAW authenticate response", ex); + } finally { + IOUtils.closeQuietly(bis); + } + } + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawRegistrationService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawRegistrationService.java new file mode 100644 index 00000000000..83ba1bfc3e8 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/RawRegistrationService.java @@ -0,0 +1,88 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +/* + * Copyright (c) 2018 Mastercard + * + * 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 io.jans.fido2.service.sg; + +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; + +import io.jans.as.model.fido.u2f.exception.BadInputException; +import io.jans.as.model.fido.u2f.message.RawRegisterResponse; +import io.jans.as.model.util.Base64Util; +import io.jans.as.model.util.SecurityProviderUtility; +import io.jans.fido2.service.Base64Service; +import io.jans.util.io.ByteDataInputStream; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +/** + * Provides operations with U2F RAW registration response + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +@ApplicationScoped +public class RawRegistrationService { + + @Inject + private Logger log; + + @Inject + private Base64Service base64Service; + + public static final byte REGISTRATION_RESERVED_BYTE_VALUE = (byte) 0x05; + public static final byte REGISTRATION_SIGNED_RESERVED_BYTE_VALUE = (byte) 0x00; + public static final long INITIAL_DEVICE_COUNTER_VALUE = -1; + + public static final String REGISTER_FINISH_TYPE = "navigator.id.finishEnrollment"; + public static final String REGISTER_CANCEL_TYPE = "navigator.id.cancelEnrollment"; + public static final String[] SUPPORTED_REGISTER_TYPES = new String[] { REGISTER_FINISH_TYPE, REGISTER_CANCEL_TYPE }; + + + public RawRegisterResponse parseRawRegisterResponse(String rawDataBase64) throws BadInputException { + ByteDataInputStream bis = new ByteDataInputStream(Base64Util.base64urldecode(rawDataBase64)); + try { + try { + byte reservedByte = bis.readSigned(); + if (reservedByte != REGISTRATION_RESERVED_BYTE_VALUE) { + throw new BadInputException("Incorrect value of reserved byte. Expected: " + REGISTRATION_RESERVED_BYTE_VALUE + ". Was: " + reservedByte); + } + return new RawRegisterResponse(bis.read(65), bis.read(bis.readUnsigned()), parseDer(bis), bis.readAll()); + } catch (IOException ex) { + throw new BadInputException("Failed to parse RAW register response", ex); + } catch (CertificateException e) { + throw new BadInputException("Malformed attestation certificate", e); + } catch (NoSuchProviderException e) { + throw new BadInputException("Failed to parse attestation certificate", e); + } + } finally { + IOUtils.closeQuietly(bis); + } + } + + public X509Certificate parseDer(InputStream is) throws CertificateException, NoSuchProviderException { + return (X509Certificate) CertificateFactory.getInstance("X.509", SecurityProviderUtility.getInstance()).generateCertificate(is); + } + + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AssertionSuperGluuController.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AssertionSuperGluuController.java new file mode 100644 index 00000000000..9cee4703ba7 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AssertionSuperGluuController.java @@ -0,0 +1,228 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service.sg.converter; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import io.jans.as.model.fido.u2f.message.RawAuthenticateResponse; +import io.jans.as.model.fido.u2f.protocol.AuthenticateResponse; +import io.jans.as.model.fido.u2f.protocol.ClientData; +import io.jans.fido2.exception.Fido2RpRuntimeException; +import io.jans.fido2.exception.Fido2RuntimeException; +import io.jans.fido2.service.AuthenticatorDataParser; +import io.jans.fido2.service.Base64Service; +import io.jans.fido2.service.DataMapperService; +import io.jans.fido2.service.DigestService; +import io.jans.fido2.service.operation.AssertionService; +import io.jans.fido2.service.persist.UserSessionIdService; +import io.jans.fido2.service.sg.RawAuthenticationService; +import io.jans.fido2.service.verifier.CommonVerifiers; +import io.jans.fido2.sg.SuperGluuMode; +import io.jans.util.StringHelper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Converters Super Gluu authentication request to U2F V2 request + * @author Yuriy Movchan + * @version Jan 26, 2023 + */ +@ApplicationScoped +public class AssertionSuperGluuController { + + @Inject + private AssertionService assertionService; + + @Inject + private DataMapperService dataMapperService; + + @Inject + private Base64Service base64Service; + + @Inject + private RawAuthenticationService rawAuthenticationService; + + @Inject + private DigestService digestService; + + @Inject + private UserSessionIdService userSessionIdService; + + /* Example for one_step: + * - request: + * username: null + * keyhandle: r4AIBCT_CEi8SWThJ-T5gsxjfZMqzqMdqCeDuK_xTvz_kr5FNNs2j6Tb2dvoXgculthxTzXF5-FI1KWsA_dRLA + * application: https://yurem-emerging-pig.gluu.info/identity/authcode.htm + * session_id: 2994e597-3dc9-4d96-ae7e-84cfcf049db6 + * - response: + * {"authenticateRequests":[{"challenge":"EELAH05XTUfPHrvpqVYhXB8pEmOMaRWY9mBurdhicBU", + * "appId":"https://yurem-emerging-pig.gluu.info/identity/authcode.htm", + * "keyHandle":"r4AIBCT_CEi8SWThJ-T5gsxjfZMqzqMdqCeDuK_xTvz_kr5FNNs2j6Tb2dvoXgculthxTzXF5-FI1KWsA_dRLA","version":"U2F_V2"}]} + * + * Example for two_step: + * - request: + * username: test1 + * keyhandle: null + * application: https://yurem-emerging-pig.gluu.info/identity/authcode.htm + * session_id: 850ff665-02b6-435b-baf8-b018b13043c3 + * - response: + * {"authenticateRequests":[{"challenge":"5QoRtudmej5trcrMRgFBoI5rZ6pzIZiYP3u3bXCvvAE", + * "appId":"https://yurem-emerging-pig.gluu.info/identity/authcode.htm", + * "keyHandle":"YJvWD9n40eIurInJvPKUoxpKzrleUMWgu9w3v_NUBu7BiGAclgkH_Zg88_T5y6Rh78imTxTh0djWFYG4jxOixw","version":"U2F_V2"}]} + */ + public JsonNode startAuthentication(String userName, String keyHandle, String appId, String sessionId) { + boolean oneStep = StringHelper.isEmpty(userName); + + boolean valid = userSessionIdService.isValidSessionId(sessionId, userName); + if (!valid) { + throw new Fido2RuntimeException(String.format("session_id '%s' is invalid", sessionId)); + } + + if (StringHelper.isEmpty(userName) && StringHelper.isEmpty(keyHandle)) { + throw new Fido2RuntimeException("The request should contains either username or keyhandle"); + } + + ObjectNode params = dataMapperService.createObjectNode(); + // Add all required parameters from request to allow process U2F request + params.put(CommonVerifiers.SUPER_GLUU_REQUEST, true); + params.put(CommonVerifiers.SUPER_GLUU_APP_ID, appId); + params.put(CommonVerifiers.SUPER_GLUU_KEY_HANDLE, keyHandle); + params.put(CommonVerifiers.SUPER_GLUU_MODE, oneStep ? SuperGluuMode.ONE_STEP.getMode() : SuperGluuMode.TWO_STEP.getMode()); + + params.put("username", userName); + params.put("session_id", sessionId); + + ObjectNode result = assertionService.options(params); + + // Build start authentication response + ObjectNode superGluuResult = dataMapperService.createObjectNode(); + ArrayNode authenticateRequests = superGluuResult.putArray("authenticateRequests"); + + String challenge = result.get("challenge").asText(); + String userVerification = result.get("userVerification").asText(); + + if (result.has("allowCredentials")) { + result.get("allowCredentials").forEach((f) -> { + ((ObjectNode) f).put("appId", appId); + ((ObjectNode) f).put("userVerification", userVerification); + ((ObjectNode) f).put("challenge", challenge); + ((ObjectNode) f).put("keyHandle", f.get("id").asText()); + ((ObjectNode) f).remove("id"); + ((ObjectNode) f).put("version", "U2F_V2"); + + authenticateRequests.add(f); + }); + } + + return superGluuResult; + } + + /* Example for one_step: + * - request: + * username: null + * tokenResponse: {"signatureData":"AQAAAAEwRQIhANrCm98JCTz6cqSZ_vwGHdF9uqe3b4z1nCrNIPCObwc-AiAblGdWyky + * LeaTJPzLtbWHMoN9MsKUlgmbfSRsINJEVeA","clientData":"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY + * 2hhbGxlbmdlIjoiRUVMQUgwNVhUVWZQSHJ2cHFWWWhYQjhwRW1PTWFSV1k5bUJ1cmRoaWNCVSIsIm9yaWdpbiI6Imh0dHBzOlwvX + * C95dXJlbS1lbWVyZ2luZy1waWcuZ2x1dS5pbmZvXC9pZGVudGl0eVwvYXV0aGNvZGUuaHRtIn0","keyHandle":"r4AIBCT_CEi + * 8SWThJ-T5gsxjfZMqzqMdqCeDuK_xTvz_kr5FNNs2j6Tb2dvoXgculthxTzXF5-FI1KWsA_dRLA"} + * - response: + * {"status":"success","challenge":"EELAH05XTUfPHrvpqVYhXB8pEmOMaRWY9mBurdhicBU"} + * + * Example for two_step: + * - request: + * username: test1 + * tokenResponse: {"signatureData":"AQAAAAEwRgIhAN4auE9-U2YDhi8ByxIIv3G2hvDeFjEGU_x5SvfcIQyUAiEA4I_xMin + * mYAmH5qk5KMaYATFAryIpoVwARGvEFQTWE2Q","clientData":"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwi + * Y2hhbGxlbmdlIjoiNVFvUnR1ZG1lajV0cmNyTVJnRkJvSTVyWjZweklaaVlQM3UzYlhDdnZBRSIsIm9yaWdpbiI6Imh0dHBzOlwv + * XC95dXJlbS1lbWVyZ2luZy1waWcuZ2x1dS5pbmZvXC9pZGVudGl0eVwvYXV0aGNvZGUuaHRtIn0","keyHandle":"YJvWD9n40e + * IurInJvPKUoxpKzrleUMWgu9w3v_NUBu7BiGAclgkH_Zg88_T5y6Rh78imTxTh0djWFYG4jxOixw"} + * - response: + * {"status":"success","challenge":"5QoRtudmej5trcrMRgFBoI5rZ6pzIZiYP3u3bXCvvAE"} + * + */ + public JsonNode finishAuthentication(String userName, String authenticateResponseString) { + AuthenticateResponse authenticateResponse; + try { + authenticateResponse = dataMapperService.readValue(authenticateResponseString, AuthenticateResponse.class); + } catch (IOException ex) { + throw new Fido2RpRuntimeException("Failed to parse options assertion request", ex); + } + + if (!authenticateResponse.getClientData().getTyp().equals(RawAuthenticationService.AUTHENTICATE_GET_TYPE)) { + throw new Fido2RuntimeException("Invalid options attestation request type"); + } + + boolean oneStep = StringHelper.isEmpty(userName); + + ObjectNode params = dataMapperService.createObjectNode(); + // Add all required parameters from request to allow process U2F request + params.put(CommonVerifiers.SUPER_GLUU_REQUEST, true); + params.put(CommonVerifiers.SUPER_GLUU_MODE, oneStep ? SuperGluuMode.ONE_STEP.getMode() : SuperGluuMode.TWO_STEP.getMode()); + + // Manadatory parameter + params.put("type", "public-key"); + + params.put("id", authenticateResponse.getKeyHandle()); + + params.put("rawId", authenticateResponseString); + + // Convert clientData node to new format + ObjectNode clientData = dataMapperService.createObjectNode(); + clientData.put("type", "webauthn.get"); + clientData.put("challenge", authenticateResponse.getClientData().getChallenge()); + clientData.put("origin", authenticateResponse.getClientData().getOrigin()); + + // Add response node + ObjectNode response = dataMapperService.createObjectNode(); + params.set("response", response); + + // We have to quote URL to conform bug in Super Gluu + response.put("clientDataJSON", base64Service.urlEncodeToString(clientData.toString().replaceAll("/", "\\\\/").getBytes(StandardCharsets.UTF_8))); + + // Prepare attestationObject + RawAuthenticateResponse rawAuthenticateResponse = rawAuthenticationService.parseRawAuthenticateResponse(authenticateResponse.getSignatureData()); + + response.put("signature", base64Service.urlEncodeToString(rawAuthenticateResponse.getSignature())); + + ObjectNode attestationObject = dataMapperService.createObjectNode(); + + try { + byte[] authData = generateAuthData(authenticateResponse.getClientData(), rawAuthenticateResponse); + response.put("authenticatorData", authData); + response.put("attestationObject", base64Service.urlEncodeToString(dataMapperService.cborWriteAsBytes(attestationObject))); + } catch (IOException e) { + throw new Fido2RuntimeException("Failed to prepare attestationObject"); + } + + ObjectNode result = assertionService.verify(params); + + result.put("status", "success"); + result.put("challenge", authenticateResponse.getClientData().getChallenge()); + + return result; + } + + private byte[] generateAuthData(ClientData clientData, RawAuthenticateResponse rawAuthenticateResponse) throws IOException { + byte[] rpIdHash = digestService.hashSha256(clientData.getOrigin()); + byte[] flags = new byte[] { AuthenticatorDataParser.FLAG_USER_PRESENT }; + byte[] counter = ByteBuffer.allocate(4).putInt((int) rawAuthenticateResponse.getCounter()).array(); + + byte[] authData = ByteBuffer + .allocate(rpIdHash.length + flags.length + counter.length) + .put(rpIdHash).put(flags).put(counter).array(); + + return authData; + } + + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java new file mode 100644 index 00000000000..bfd73abb2c6 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java @@ -0,0 +1,276 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service.sg.converter; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import io.jans.as.model.fido.u2f.message.RawRegisterResponse; +import io.jans.as.model.fido.u2f.protocol.ClientData; +import io.jans.as.model.fido.u2f.protocol.RegisterResponse; +import io.jans.fido2.ctap.AttestationFormat; +import io.jans.fido2.exception.Fido2RpRuntimeException; +import io.jans.fido2.exception.Fido2RuntimeException; +import io.jans.fido2.model.conf.AppConfiguration; +import io.jans.fido2.service.AuthenticatorDataParser; +import io.jans.fido2.service.Base64Service; +import io.jans.fido2.service.CoseService; +import io.jans.fido2.service.DataMapperService; +import io.jans.fido2.service.DigestService; +import io.jans.fido2.service.operation.AttestationService; +import io.jans.fido2.service.persist.UserSessionIdService; +import io.jans.fido2.service.sg.RawRegistrationService; +import io.jans.fido2.service.verifier.CommonVerifiers; +import io.jans.fido2.sg.SuperGluuMode; +import io.jans.service.net.NetworkService; +import io.jans.util.StringHelper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Converters Super Gluu registration request to U2F V2 request + * @author Yuriy Movchan + * @version Jan 26, 2023 + */ +@ApplicationScoped +public class AttestationSuperGluuController { + + @Inject + private AttestationService attestationService; + + @Inject + private DataMapperService dataMapperService; + + @Inject + private CommonVerifiers commonVerifiers; + + @Inject + private Base64Service base64Service; + + @Inject + private RawRegistrationService rawRegistrationService; + + @Inject + private CoseService coseService; + + @Inject + private NetworkService networkService; + + @Inject + private DigestService digestService; + + @Inject + private UserSessionIdService userSessionIdService; + + @Inject + private AppConfiguration appConfiguration; + + /* Example for one_step: + * - request: + * username: null + * application: https://yurem-emerging-pig.gluu.info/identity/authcode.htm + * session_id: a5183b05-dbe0-4173-a794-2334bb864708 + * enrollment_code: null + * - response: + * {"authenticateRequests":[],"registerRequests":[{"challenge":"GU4usvpYfvQ_RCMSqm819gTZMa0qCeLr1Xg2KbvW2To" + * "appId":"https://yurem-emerging-pig.gluu.info/identity/authcode.htm","version":"U2F_V2"}]} + * + * Example for two_step: + * - request: + * username: test1 + * application: https://yurem-emerging-pig.gluu.info/identity/authcode.htm + * session_id: 46c30db8-0339-4459-8ee7-e4960ad75986 + * enrollment_code: null + * - response: + * {"authenticateRequests":[],"registerRequests":[{"challenge":"raPfmqZOHlHF4gXbprd29uwX-bs3Ff5v03quxBD4FkM", + * "appId":"https://yurem-emerging-pig.gluu.info/identity/authcode.htm","version":"U2F_V2"}]} + */ + public JsonNode startRegistration(String userName, String appId, String sessionId, String enrollmentCode) { + boolean oneStep = StringHelper.isEmpty(userName); + + boolean valid = userSessionIdService.isValidSessionId(sessionId, userName); + if (!valid) { + throw new Fido2RuntimeException(String.format("session_id '%s' is invalid", sessionId)); + } + + ObjectNode params = dataMapperService.createObjectNode(); + // Add all required parameters from request to allow process U2F request + params.put(CommonVerifiers.SUPER_GLUU_REQUEST, true); + params.put(CommonVerifiers.SUPER_GLUU_MODE, oneStep ? SuperGluuMode.ONE_STEP.getMode() : SuperGluuMode.TWO_STEP.getMode()); + params.put(CommonVerifiers.SUPER_GLUU_APP_ID, appId); + + String useUserName = userName; + if (oneStep) { + useUserName = attestationService.generateUserId(); + } + + params.put("username", useUserName); + params.put("displayName", useUserName); + + params.put("session_id", sessionId); + + // Required parameters + params.put("attestation", "direct"); + + ObjectNode result = attestationService.options(params); + + // Build start registration response + ObjectNode superGluuResult = dataMapperService.createObjectNode(); + ArrayNode registerRequests = superGluuResult.putArray("registerRequests"); + + result.put("appId", appId); + registerRequests.add(result); + + result.put("version", "U2F_V2"); + + return superGluuResult; + } + + /* Example for one_step: + * - request: + * username: null + * tokenResponse: {"registrationData":"BQQTkZFzsbTmuUoS_DS_jqpWRbZHp_J0YV8q4Xb4XTPYbIuvu-TRNubp8U-CKZuB + * 5tDT-l6R3sQvNc6wXjGCmL-OQK-ACAQk_whIvElk4Sfk-YLMY32TKs6jHagng7iv8U78_5K-RTTbNo-k29nb6F4HLpbYcU81xefh + * SNSlrAP3USwwggImMIIBzKADAgECAoGBAPMsD5b5G58AphKuKWl4Yz27sbE_rXFy7nPRqtJ_r4E5DSZbFvfyuos-Db0095ubB0Jo + * yM8ccmSO_eZQ6IekOLPKCR7yC5kes-f7MaxyaphmmD4dEvmuKjF-fRsQP5tQG7zerToto8eIz0XjPaupiZxQXtSHGHHTuPhri2nf + * oZlrMAoGCCqGSM49BAMCMFwxIDAeBgNVBAMTF0dsdXUgb3hQdXNoMiBVMkYgdjEuMC4wMQ0wCwYDVQQKEwRHbHV1MQ8wDQYDVQQH + * EwZBdXN0aW4xCzAJBgNVBAgTAlRYMQswCQYDVQQGEwJVUzAeFw0xNjAzMDExODU5NDZaFw0xOTAzMDExODU5NDZaMFwxIDAeBgNV + * BAMTF0dsdXUgb3hQdXNoMiBVMkYgdjEuMC4wMQ0wCwYDVQQKEwRHbHV1MQ8wDQYDVQQHEwZBdXN0aW4xCzAJBgNVBAgTAlRYMQsw + * CQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABICUKnzCE5PJ7tihiKkYu6E5Uy_sZ-RSqs_MnUJt0tB8G8GSg9nK + * o6P2424iV9lXX9Pil8qw4ofZ-fAXXepbp4MwCgYIKoZIzj0EAwIDSAAwRQIgUWwawAB2udURWQziDXVjSOi_QcuXiRxylqj5thFw + * FhYCIQCGY-CTZFi7JdkhZ05nDpbSYJBTOo1Etckh7k0qcvnO0TBFAiEA1v1jKTwGn5LRRGSab1kNdgEqD6qL08bougoJUNY1A5MC + * IGvtBFSNzhGvhQmdYYj5-XOd5P4ucVk6TmkV1Xu73Dvj","clientData":"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5y + * b2xsbWVudCIsImNoYWxsZW5nZSI6IkdVNHVzdnBZZnZRX1JDTVNxbTgxOWdUWk1hMHFDZUxyMVhnMktidlcyVG8iLCJvcmlnaW4i + * OiJodHRwczpcL1wveXVyZW0tZW1lcmdpbmctcGlnLmdsdXUuaW5mbyJ9","deviceData":"eyJuYW1lIjoiU00tRzk5MUIiLCJv + * c19uYW1lIjoidGlyYW1pc3UiLCJvc192ZXJzaW9uIjoiMTMiLCJwbGF0Zm9ybSI6ImFuZHJvaWQiLCJwdXNoX3Rva2VuIjoicHVz + * aF90b2tlbiIsInR5cGUiOiJub3JtYWwiLCJ1dWlkIjoidXVpZCJ9"} + * - response: + * {"status":"success","challenge":"GU4usvpYfvQ_RCMSqm819gTZMa0qCeLr1Xg2KbvW2To"} + * + * Example for two_step: + * - request: + * username: test1 + * tokenResponse: {"registrationData":"BQToXkGAjgXxC4g1NiA-IuRAu40NFBlXXNSu4TEZqGK5TBqwU07ANn4LJ9Hp3aV5 + * PIvCDVsQ2tZJf1xD6LosZNDuQGCb1g_Z-NHiLqyJybzylKMaSs65XlDFoLvcN7_zVAbuwYhgHJYJB_2YPPP0-cukYe_Ipk8U4dHY + * 1hWBuI8ToscwggImMIIBzKADAgECAoGBAPMsD5b5G58AphKuKWl4Yz27sbE_rXFy7nPRqtJ_r4E5DSZbFvfyuos-Db0095ubB0Jo + * yM8ccmSO_eZQ6IekOLPKCR7yC5kes-f7MaxyaphmmD4dEvmuKjF-fRsQP5tQG7zerToto8eIz0XjPaupiZxQXtSHGHHTuPhri2nf + * oZlrMAoGCCqGSM49BAMCMFwxIDAeBgNVBAMTF0dsdXUgb3hQdXNoMiBVMkYgdjEuMC4wMQ0wCwYDVQQKEwRHbHV1MQ8wDQYDVQQH + * EwZBdXN0aW4xCzAJBgNVBAgTAlRYMQswCQYDVQQGEwJVUzAeFw0xNjAzMDExODU5NDZaFw0xOTAzMDExODU5NDZaMFwxIDAeBgNV + * BAMTF0dsdXUgb3hQdXNoMiBVMkYgdjEuMC4wMQ0wCwYDVQQKEwRHbHV1MQ8wDQYDVQQHEwZBdXN0aW4xCzAJBgNVBAgTAlRYMQsw + * CQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABICUKnzCE5PJ7tihiKkYu6E5Uy_sZ-RSqs_MnUJt0tB8G8GSg9nK + * o6P2424iV9lXX9Pil8qw4ofZ-fAXXepbp4MwCgYIKoZIzj0EAwIDSAAwRQIgUWwawAB2udURWQziDXVjSOi_QcuXiRxylqj5thFw + * FhYCIQCGY-CTZFi7JdkhZ05nDpbSYJBTOo1Etckh7k0qcvnO0TBFAiArOYmHd22USw7flCmGXLOXVOrhDi-pkX7Qx_c8oz5hJQIh + * AOslo3LfymoFWT6mxUZjBlxKgxioozd0KmzUwobRcKdW","clientData":"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5y + * b2xsbWVudCIsImNoYWxsZW5nZSI6InJhUGZtcVpPSGxIRjRnWGJwcmQyOXV3WC1iczNGZjV2MDNxdXhCRDRGa00iLCJvcmlnaW4i + * OiJodHRwczpcL1wveXVyZW0tZW1lcmdpbmctcGlnLmdsdXUuaW5mbyJ9","deviceData":"eyJuYW1lIjoiU00tRzk5MUIiLCJv + * c19uYW1lIjoidGlyYW1pc3UiLCJvc192ZXJzaW9uIjoiMTMiLCJwbGF0Zm9ybSI6ImFuZHJvaWQiLCJwdXNoX3Rva2VuIjoicHVz + * aF90b2tlbiIsInR5cGUiOiJub3JtYWwiLCJ1dWlkIjoidXVpZCJ9"} + * - response: + * {"status":"success","challenge":"raPfmqZOHlHF4gXbprd29uwX-bs3Ff5v03quxBD4FkM"} + * + */ + public JsonNode finishRegistration(String userName, String registerResponseString) { + RegisterResponse registerResponse; + try { + registerResponse = dataMapperService.readValue(registerResponseString, RegisterResponse.class); + } catch (IOException ex) { + throw new Fido2RpRuntimeException("Failed to parse options attestation request", ex); + } + + if (!registerResponse.getClientData().getTyp().equals(RawRegistrationService.REGISTER_FINISH_TYPE)) { + throw new Fido2RuntimeException("Invalid options attestation request type"); + } + + ObjectNode params = dataMapperService.createObjectNode(); + // Add all required parameters from request to allow process U2F request + params.put(CommonVerifiers.SUPER_GLUU_REQUEST, true); + boolean oneStep = StringHelper.isEmpty(userName); + params.put(CommonVerifiers.SUPER_GLUU_MODE, oneStep ? SuperGluuMode.ONE_STEP.getMode() : SuperGluuMode.TWO_STEP.getMode()); + + // Manadatory parameter + params.put("type", "public-key"); + + // Add response node + ObjectNode response = dataMapperService.createObjectNode(); + params.set("response", response); + response.put("deviceData", registerResponse.getDeviceData()); + + // Convert clientData node to new format + ObjectNode clientData = dataMapperService.createObjectNode(); + clientData.put("challenge", registerResponse.getClientData().getChallenge()); + clientData.put("origin", registerResponse.getClientData().getOrigin()); + clientData.put("type", "webauthn.create"); + response.put("clientDataJSON", base64Service.urlEncodeToString(clientData.toString().getBytes(Charset.forName("UTF-8")))); + + // Prepare attestationObject + RawRegisterResponse rawRegisterResponse = rawRegistrationService.parseRawRegisterResponse(registerResponse.getRegistrationData()); + + params.put("id", base64Service.urlEncodeToString(rawRegisterResponse.getKeyHandle())); + + ObjectNode attestationObject = dataMapperService.createObjectNode(); + ObjectNode attStmt = dataMapperService.createObjectNode(); + + try { + ArrayNode x5certs = attStmt.putArray("x5c"); + x5certs.add(base64Service.encodeToString(rawRegisterResponse.getAttestationCertificate().getEncoded())); + attStmt.put("sig", rawRegisterResponse.getSignature()); + + attestationObject.put("fmt", AttestationFormat.fido_u2f_super_gluu.getFmt()); + attestationObject.set("attStmt", attStmt); + + byte[] authData = generateAuthData(registerResponse.getClientData(), rawRegisterResponse); + attestationObject.put("authData", authData); + + response.put("attestationObject", base64Service.urlEncodeToString(dataMapperService.cborWriteAsBytes(attestationObject))); + } catch (CertificateEncodingException e) { + } catch (IOException e) { + throw new Fido2RuntimeException("Failed to prepare attestationObject"); + } + + ObjectNode result = attestationService.verify(params); + + result.put("status", "success"); + result.put("challenge", registerResponse.getClientData().getChallenge()); + + return result; + } + + private byte[] generateAuthData(ClientData clientData, RawRegisterResponse rawRegisterResponse) throws IOException { + byte[] rpIdHash = digestService.hashSha256(clientData.getOrigin()); + byte[] flags = new byte[] { AuthenticatorDataParser.FLAG_USER_PRESENT | AuthenticatorDataParser.FLAG_ATTESTED_CREDENTIAL_DATA_INCLUDED }; + byte[] counter = ByteBuffer.allocate(4).putInt(0).array(); + + byte[] aaguid = ByteBuffer.allocate(16).array(); + + byte[] credIDBuffer = rawRegisterResponse.getKeyHandle(); + + byte[] credIDLenBuffer = ByteBuffer.allocate(2).putShort((short) credIDBuffer.length).array(); + + + JsonNode uncompressedECPoint = coseService.convertECKeyToUncompressedPoint( + rawRegisterResponse.getUserPublicKey()); + + byte[] cosePublicKeyBuffer = dataMapperService.cborWriteAsBytes(uncompressedECPoint); + + byte[] authData = ByteBuffer + .allocate(rpIdHash.length + flags.length + counter.length + aaguid.length + credIDLenBuffer.length + + credIDBuffer.length + cosePublicKeyBuffer.length) + .put(rpIdHash).put(flags).put(counter).put(aaguid).put(credIDLenBuffer).put(credIDBuffer) + .put(cosePublicKeyBuffer).array(); + + return authData; + } + +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/CustomScriptService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/CustomScriptService.java index 7e05ea777fc..8765ded4dca 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/CustomScriptService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/CustomScriptService.java @@ -11,7 +11,6 @@ import jakarta.enterprise.inject.Alternative; import jakarta.inject.Inject; import jakarta.interceptor.Interceptor; - import io.jans.as.model.config.StaticConfiguration; import io.jans.service.custom.script.AbstractCustomScriptService; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java index ad61e8907c2..702ea4b6c8a 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java @@ -13,8 +13,8 @@ import io.jans.fido2.model.conf.AppConfiguration; import io.jans.model.ApplicationType; -import io.jans.as.model.config.StaticConfiguration; import io.jans.as.common.service.common.ApplicationFactory; +import io.jans.as.model.config.StaticConfiguration; import io.jans.orm.PersistenceEntryManager; import io.jans.service.metric.inject.ReportMetric; import io.jans.service.net.NetworkService; @@ -64,10 +64,6 @@ public io.jans.service.metric.MetricService getMetricServiceInstance() { @Override public boolean isMetricReporterEnabled() { - if (this.appConfiguration.getMetricReporterEnabled() == null) { - return false; - } - return this.appConfiguration.getMetricReporterEnabled(); } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AssertionVerifier.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AssertionVerifier.java index 83e276b49da..254b67cbc7d 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AssertionVerifier.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AssertionVerifier.java @@ -18,12 +18,12 @@ package io.jans.fido2.service.verifier; +import io.jans.orm.model.fido2.Fido2AuthenticationData; import io.jans.orm.model.fido2.Fido2RegistrationData; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import io.jans.fido2.exception.Fido2RuntimeException; -import io.jans.fido2.model.entry.Fido2AuthenticationData; import io.jans.fido2.service.processor.assertion.AssertionProcessorFactory; import io.jans.fido2.service.processors.AssertionFormatProcessor; import org.slf4j.Logger; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AuthenticatorDataVerifier.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AuthenticatorDataVerifier.java index e667bc6ed7b..e0c410b2ff4 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AuthenticatorDataVerifier.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/AuthenticatorDataVerifier.java @@ -118,6 +118,12 @@ public void verifyAssertionSignature(AuthData authData, byte[] clientDataHash, S byte[] signatureBase = ByteBuffer.allocate(bufferSize).put(rpIdHash).put(flags).put(counters).put(extensionsBuffer).put(clientDataHash).array(); byte[] signatureBytes = base64Service.urlDecode(signature.getBytes()); + log.debug("RP hash {}", Hex.encodeHexString(rpIdHash)); + log.debug("Flags {}", Hex.encodeHexString(flags)); + log.debug("Counters {}", Hex.encodeHexString(counters)); + log.debug("Extensions {}", Hex.encodeHexString(extensionsBuffer)); + log.debug("Client(channlenge) data hash {}", Hex.encodeHexString(clientDataHash)); + log.debug("Signature {}", Hex.encodeHexString(signatureBytes)); log.debug("Signature Base {}", Hex.encodeHexString(signatureBase)); log.debug("Signature BaseLen {}", signatureBase.length); @@ -140,10 +146,15 @@ private byte[] convertCOSEtoPublicKey(byte[] cosePublicKey) { public void verifyU2FAttestationSignature(AuthData authData, byte[] clientDataHash, String signature, Certificate certificate, int signatureAlgorithm) { + verifyU2FAttestationSignature(authData, authData.getRpIdHash(), clientDataHash, signature, certificate, + signatureAlgorithm); + } + + public void verifyU2FAttestationSignature(AuthData authData, byte[] rpIdHash, byte[] clientDataHash, String signature, Certificate certificate, + int signatureAlgorithm) { int bufferSize = 0; byte[] reserved = new byte[] { 0x00 }; bufferSize += reserved.length; - byte[] rpIdHash = authData.getRpIdHash(); bufferSize += rpIdHash.length; bufferSize += clientDataHash.length; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/CommonVerifiers.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/CommonVerifiers.java index 48d67bd5198..bf77dc323aa 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/CommonVerifiers.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/CommonVerifiers.java @@ -11,18 +11,18 @@ import java.nio.charset.Charset; import java.util.Arrays; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; - import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; + import io.jans.fido2.ctap.AttestationConveyancePreference; import io.jans.fido2.ctap.AuthenticatorAttachment; import io.jans.fido2.ctap.TokenBindingSupport; -import io.jans.fido2.ctap.UserVerification; import io.jans.fido2.exception.Fido2CompromisedDevice; +import io.jans.fido2.exception.Fido2RpRuntimeException; import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.auth.AuthData; import io.jans.fido2.model.auth.CredAndCounterData; @@ -30,11 +30,13 @@ import io.jans.fido2.service.Base64Service; import io.jans.fido2.service.DataMapperService; import io.jans.fido2.service.processors.AttestationFormatProcessor; +import io.jans.fido2.sg.SuperGluuMode; +import io.jans.orm.model.fido2.UserVerification; import io.jans.service.net.NetworkService; import io.jans.util.StringHelper; -import org.slf4j.Logger; - -import com.fasterxml.jackson.databind.JsonNode; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; /** * @author Yuriy Movchan @@ -43,7 +45,12 @@ @ApplicationScoped public class CommonVerifiers { - @Inject + public static final String SUPER_GLUU_REQUEST = "super_gluu_request"; + public static final String SUPER_GLUU_MODE = "super_gluu_request_mode"; + public static final String SUPER_GLUU_APP_ID = "super_gluu_app_id"; + public static final String SUPER_GLUU_KEY_HANDLE = "super_gluu_key_handle"; + + @Inject private Logger log; @Inject @@ -309,6 +316,14 @@ public JsonNode verifyClientJSON(JsonNode responseNode) { return clientJsonNode; } + public JsonNode verifyClientRaw(JsonNode responseNode) { + if (!responseNode.hasNonNull("clientDataRaw")) { + throw new Fido2RuntimeException("Client data RAW is missing"); + } + + return responseNode.get("clientDataRaw"); + } + public void verifyTPMVersion(JsonNode ver) { if (!"2.0".equals(ver.asText())) { throw new Fido2RuntimeException("Invalid TPM Attestation version"); @@ -417,10 +432,10 @@ public String verifyCredentialId(CredAndCounterData attestationData, JsonNode pa throw new Fido2RuntimeException("Credential id attestationObject and response id mismatch"); } -// String attestationDataCredId = attestationData.getCredId(); -// if (!StringHelper.compare(attestationDataCredId, paramsKeyId)) { -// throw new Fido2RPRuntimeException("Credential id attestationObject and response id mismatch"); -// } + String attestationDataCredId = attestationData.getCredId(); + if (!StringHelper.compare(attestationDataCredId, paramsKeyId)) { + throw new Fido2RuntimeException("Credential id attestationObject and response id mismatch"); + } return paramsKeyId; } @@ -462,4 +477,30 @@ public void verifyThatMetadataIsValid(JsonNode metadata) { } } + public boolean hasSuperGluu(JsonNode params) { + if (params.hasNonNull(SUPER_GLUU_REQUEST)) { + JsonNode node = params.get(SUPER_GLUU_REQUEST); + return node.isBoolean() && node.asBoolean(); + } + + return false; + } + + public void verifyNotUseGluuParameters(JsonNode params) { + // Protect generic U2F/Fido2 from sending requests with Super Gluu parameters + if (params.hasNonNull(SUPER_GLUU_REQUEST) || params.hasNonNull(SUPER_GLUU_MODE) || + params.hasNonNull(SUPER_GLUU_APP_ID) || params.hasNonNull(SUPER_GLUU_KEY_HANDLE)) { + throw new Fido2RpRuntimeException("Input request conflicts with Super Gluu parameters"); + } + } + + public boolean isSuperGluuOneStepMode(JsonNode params) { + if (hasSuperGluu(params)) { + return SuperGluuMode.ONE_STEP == SuperGluuMode.fromModeValue(params.get(CommonVerifiers.SUPER_GLUU_MODE).asText()); + } + + return false; + } + + } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/UserVerificationVerifier.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/UserVerificationVerifier.java index aab25e88c01..920eba8d881 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/UserVerificationVerifier.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/verifier/UserVerificationVerifier.java @@ -10,10 +10,12 @@ import jakarta.inject.Inject; import org.apache.commons.codec.binary.Hex; -import io.jans.fido2.ctap.UserVerification; + import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.auth.AuthData; import io.jans.fido2.service.AuthenticatorDataParser; +import io.jans.orm.model.fido2.UserVerification; + import org.slf4j.Logger; /** diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AssertionController.java b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AssertionController.java index 5ccd6b86000..cd4ca05108a 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AssertionController.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AssertionController.java @@ -7,24 +7,49 @@ package io.jans.fido2.ws.rs.controller; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; + +import io.jans.as.model.fido.u2f.message.RawAuthenticateResponse; +import io.jans.as.model.fido.u2f.protocol.AuthenticateResponse; +import io.jans.as.model.fido.u2f.protocol.ClientData; +import io.jans.fido2.exception.Fido2RpRuntimeException; +import io.jans.fido2.exception.Fido2RuntimeException; +import io.jans.fido2.model.conf.AppConfiguration; +import io.jans.fido2.service.AuthenticatorDataParser; +import io.jans.fido2.service.Base64Service; +import io.jans.fido2.service.DataMapperService; +import io.jans.fido2.service.operation.AssertionService; +import io.jans.fido2.service.sg.RawAuthenticationService; +import io.jans.fido2.service.sg.converter.AssertionSuperGluuController; +import io.jans.fido2.service.verifier.CommonVerifiers; +import io.jans.fido2.sg.SuperGluuMode; +import io.jans.util.StringHelper; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.Status; -import io.jans.fido2.exception.Fido2RpRuntimeException; -import io.jans.fido2.model.conf.AppConfiguration; -import io.jans.fido2.service.DataMapperService; -import io.jans.fido2.service.operation.AssertionService; - -import com.fasterxml.jackson.databind.JsonNode; - /** * serves request for /assertion endpoint exposed by FIDO2 sever * @author Yuriy Movchan @@ -39,10 +64,16 @@ public class AssertionController { @Inject private DataMapperService dataMapperService; + + @Inject + private AssertionSuperGluuController assertionSuperGluuController; @Inject private AppConfiguration appConfiguration; + @Inject + private CommonVerifiers commonVerifiers; + @POST @Consumes({ "application/json" }) @Produces({ "application/json" }) @@ -59,6 +90,7 @@ public Response authenticate(String content) { throw new Fido2RpRuntimeException("Failed to parse options assertion request", ex); } + commonVerifiers.verifyNotUseGluuParameters(params); JsonNode result = assertionService.options(params); ResponseBuilder builder = Response.ok().entity(result.toString()); @@ -81,10 +113,39 @@ public Response verify(String content) { throw new Fido2RpRuntimeException("Failed to parse finish assertion request", ex); } + commonVerifiers.verifyNotUseGluuParameters(params); JsonNode result = assertionService.verify(params); ResponseBuilder builder = Response.ok().entity(result.toString()); return builder.build(); } + @GET + @Produces({ "application/json" }) + @Path("/authentication") + public Response startAuthentication(@QueryParam("username") String userName, @QueryParam("keyhandle") String keyHandle, @QueryParam("application") String appId, @QueryParam("session_id") String sessionId) { + if ((appConfiguration.getFido2Configuration() == null) && !appConfiguration.isSuperGluuEnabled()) { + return Response.status(Status.FORBIDDEN).build(); + } + + JsonNode result = assertionSuperGluuController.startAuthentication(userName, keyHandle, appId, sessionId); + + ResponseBuilder builder = Response.ok().entity(result.toString()); + return builder.build(); + } + + @POST + @Produces({ "application/json" }) + @Path("/authentication") + public Response finishAuthentication(@FormParam("username") String userName, @FormParam("tokenResponse") String authenticateResponseString) { + if ((appConfiguration.getFido2Configuration() == null) && !appConfiguration.isSuperGluuEnabled()) { + return Response.status(Status.FORBIDDEN).build(); + } + + JsonNode result = assertionSuperGluuController.finishAuthentication(userName, authenticateResponseString); + + ResponseBuilder builder = Response.ok().entity(result.toString()); + return builder.build(); + } + } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AttestationController.java b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AttestationController.java index 6967b9a9515..add3eb065a7 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AttestationController.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/AttestationController.java @@ -8,23 +8,27 @@ import java.io.IOException; +import com.fasterxml.jackson.databind.JsonNode; + +import io.jans.fido2.exception.Fido2RpRuntimeException; +import io.jans.fido2.model.conf.AppConfiguration; +import io.jans.fido2.service.DataMapperService; +import io.jans.fido2.service.operation.AttestationService; +import io.jans.fido2.service.sg.converter.AttestationSuperGluuController; +import io.jans.fido2.service.verifier.CommonVerifiers; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.Status; -import io.jans.fido2.exception.Fido2RpRuntimeException; -import io.jans.fido2.model.conf.AppConfiguration; -import io.jans.fido2.service.DataMapperService; -import io.jans.fido2.service.operation.AttestationService; - -import com.fasterxml.jackson.databind.JsonNode; - /** * serves request for /attestation endpoint exposed by FIDO2 sever * @author Yuriy Movchan @@ -40,6 +44,12 @@ public class AttestationController { @Inject private DataMapperService dataMapperService; + @Inject + private CommonVerifiers commonVerifiers; + + @Inject + private AttestationSuperGluuController attestationSuperGluuController; + @Inject private AppConfiguration appConfiguration; @@ -59,6 +69,7 @@ public Response register(String content) { throw new Fido2RpRuntimeException("Failed to parse options attestation request", ex); } + commonVerifiers.verifyNotUseGluuParameters(params); JsonNode result = attestationService.options(params); ResponseBuilder builder = Response.ok().entity(result.toString()); @@ -81,9 +92,40 @@ public Response verify(String content) { throw new Fido2RpRuntimeException("Failed to parse finish attestation request", ex) ; } + commonVerifiers.verifyNotUseGluuParameters(params); JsonNode result = attestationService.verify(params); ResponseBuilder builder = Response.ok().entity(result.toString()); return builder.build(); } + + @GET + @Produces({ "application/json" }) + @Path("/registration") + public Response startRegistration(@QueryParam("username") String userName, @QueryParam("application") String appId, @QueryParam("session_id") String sessionId, @QueryParam("enrollment_code") String enrollmentCode) { + if ((appConfiguration.getFido2Configuration() == null) && !appConfiguration.isSuperGluuEnabled()) { + return Response.status(Status.FORBIDDEN).build(); + } + + JsonNode result = attestationSuperGluuController.startRegistration(userName, appId, sessionId, enrollmentCode); + + ResponseBuilder builder = Response.ok().entity(result.toString()); + return builder.build(); + } + + @POST + @Produces({ "application/json" }) + @Path("/registration") + public Response finishRegistration(@FormParam("username") String userName, @FormParam("tokenResponse") String registerResponseString) { + if ((appConfiguration.getFido2Configuration() == null) && !appConfiguration.isSuperGluuEnabled()) { + return Response.status(Status.FORBIDDEN).build(); + } + + JsonNode result = attestationSuperGluuController.finishRegistration(userName, registerResponseString); + + ResponseBuilder builder = Response.ok().entity(result.toString()); + return builder.build(); + + } + } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/ConfigurationController.java b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/ConfigurationController.java index c98ab4e9182..1d6007862df 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/ConfigurationController.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/ConfigurationController.java @@ -60,6 +60,11 @@ public Response getConfiguration() { assertion.put("base_path", baseEndpointUri + "/assertion"); assertion.put("options_enpoint", baseEndpointUri + "/assertion/options"); assertion.put("result_enpoint", baseEndpointUri + "/assertion/result"); + + if (appConfiguration.isSuperGluuEnabled()) { + response.put("super_gluu_registration_endpoint", baseEndpointUri + "/attestation/registration"); + response.put("super_gluu_authentication_endpoint", baseEndpointUri + "/assertion/authentication"); + } ResponseBuilder builder = Response.ok().entity(response.toString()); return builder.build(); diff --git a/jans-fido2/server/src/main/java/io/jans/u2f/service/persist/DeviceRegistrationService.java b/jans-fido2/server/src/main/java/io/jans/u2f/service/persist/DeviceRegistrationService.java index 2e54d8ff002..352d2ff1516 100644 --- a/jans-fido2/server/src/main/java/io/jans/u2f/service/persist/DeviceRegistrationService.java +++ b/jans-fido2/server/src/main/java/io/jans/u2f/service/persist/DeviceRegistrationService.java @@ -26,7 +26,6 @@ import io.jans.fido2.service.CoseService; import io.jans.fido2.service.DataMapperService; import io.jans.fido2.service.persist.RegistrationPersistenceService; -import io.jans.as.model.config.StaticConfiguration; /* * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. @@ -35,6 +34,7 @@ */ import io.jans.as.common.service.common.UserService; +import io.jans.as.model.config.StaticConfiguration; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.model.base.SimpleBranch; import io.jans.orm.search.filter.Filter; @@ -123,7 +123,7 @@ public void migrateToFido2(List fidoRegistrations, String do // Save converted Fido2 entry Date enrollmentDate = fidoRegistration.getCreationDate(); - Fido2RegistrationEntry fido2RegistrationEntry = registrationPersistenceService.buildFido2RegistrationEntry(fido2RegistrationData); + Fido2RegistrationEntry fido2RegistrationEntry = registrationPersistenceService.buildFido2RegistrationEntry(fido2RegistrationData, false); // Restore dates modified by buildFido2RegistrationEntry fido2RegistrationEntry.getRegistrationData().setCreatedDate(enrollmentDate); diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index f76f8815d4c..5d3003d176f 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -2870,6 +2870,16 @@ "syntax": "1.3.6.1.4.1.1466.115.121.1.15", "x_origin": "Jans created attribute" }, + { + "desc": "jansPublicKeyIdHash", + "equality": "integerMatch", + "names": [ + "jansPublicKeyIdHash" + ], + "oid": "jansAttr", + "syntax": "1.3.6.1.4.1.1466.115.121.1.27", + "x_origin": "Jans created attribute" + }, { "desc": "Life of access token", "equality": "integerMatch", @@ -4147,11 +4157,15 @@ "may": [ "jansId", "creationDate", + "jansApp", "jansSessStateId", "jansCodeChallenge", + "jansCodeChallengeHash", "personInum", "jansAuthData", - "jansStatus" + "jansStatus", + "exp", + "del" ], "must": [ "objectclass" @@ -4171,15 +4185,20 @@ "jansId", "creationDate", "displayName", + "jansApp", "jansSessStateId", "jansCodeChallenge", "jansCodeChallengeHash", "jansPublicKeyId", + "jansPublicKeyIdHash", "personInum", "jansRegistrationData", + "jansDeviceData", "jansDeviceNotificationConf", "jansCounter", - "jansStatus" + "jansStatus", + "exp", + "del" ], "must": [ "objectclass" diff --git a/jans-linux-setup/jans_setup/static/opendj/index.json b/jans-linux-setup/jans_setup/static/opendj/index.json index c9f5387eada..dd5c9d94ef4 100644 --- a/jans-linux-setup/jans_setup/static/opendj/index.json +++ b/jans-linux-setup/jans_setup/static/opendj/index.json @@ -242,6 +242,13 @@ "backend": ["userRoot"] }, + { + "attribute": "jansPublicKeyIdHash", + "type": "integer", + "index": ["equality"], + "backend": ["userRoot"] + }, + { "attribute": "jansAttrs", "type": "string", diff --git a/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json b/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json index e995ccf0b83..573091219b4 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json +++ b/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json @@ -203,17 +203,29 @@ "fields": [ "creationDate", "jansStatus", - "personInum" + "personInum", + "jansApp", + "jansCodeChallenge", + "jansCodeChallengeHash" ], - "custom": [] + "custom": [ + "`del`, `exp`" + ] }, "jansFido2RegistrationEntry": { "fields": [ "creationDate", "jansStatus", - "personInum" + "personInum", + "jansApp", + "jansPublicKeyId", + "jansPublicKeyIdHash", + "jansCodeChallenge", + "jansCodeChallengeHash" ], - "custom": [] + "custom": [ + "`del`, `exp`" + ] }, "jansInumMap": { "fields": [ diff --git a/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json b/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json index 423312d2e88..cfb11fcb8a9 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json +++ b/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json @@ -203,17 +203,29 @@ "fields": [ "creationDate", "jansStatus", - "personInum" + "personInum", + "jansApp", + "jansCodeChallenge", + "jansCodeChallengeHash" ], - "custom": [] + "custom": [ + "(\"del\", \"exp\")" + ] }, "jansFido2RegistrationEntry": { "fields": [ "creationDate", "jansStatus", - "personInum" + "personInum", + "jansApp", + "jansPublicKeyId", + "jansPublicKeyIdHash" + "jansCodeChallenge", + "jansCodeChallengeHash" ], - "custom": [] + "custom": [ + "(\"del\", \"exp\")" + ] }, "jansInumMap": { "fields": [ diff --git a/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json b/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json index 68f68f525bd..97f888d9767 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json +++ b/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json @@ -194,17 +194,29 @@ "fields": [ "creationDate", "jansStatus", - "personInum" + "personInum", + "jansApp", + "jansCodeChallenge", + "jansCodeChallengeHash" ], - "custom": [] + "custom": [ + "`del`, `exp`" + ] }, "jansFido2RegistrationEntry": { "fields": [ "creationDate", "jansStatus", - "personInum" + "personInum", + "jansApp", + "jansPublicKeyId", + "jansPublicKeyIdHash", + "jansCodeChallenge", + "jansCodeChallengeHash" ], - "custom": [] + "custom": [ + "`del`, `exp`" + ] }, "jansInumMap": { "fields": [ diff --git a/jans-linux-setup/jans_setup/templates/base.ldif b/jans-linux-setup/jans_setup/templates/base.ldif index 4241234eaa0..89e11948f93 100644 --- a/jans-linux-setup/jans_setup/templates/base.ldif +++ b/jans-linux-setup/jans_setup/templates/base.ldif @@ -98,25 +98,20 @@ objectClass: top objectClass: organizationalUnit ou: device -dn: ou=u2f,o=jans +dn: ou=fido2,o=jans objectClass: top objectClass: organizationalUnit -ou: u2f +ou: fido2 -dn: ou=registration_requests,ou=u2f,o=jans +dn: ou=fido2_register,ou=fido2,o=jans objectClass: top objectClass: organizationalUnit -ou: registration_requests +ou: fido2_register -dn: ou=authentication_requests,ou=u2f,o=jans +dn: ou=fido2_auth,ou=fido2,o=jans objectClass: top objectClass: organizationalUnit -ou: authentication_requests - -dn: ou=registered_devices,ou=u2f,o=jans -objectClass: top -objectClass: organizationalUnit -ou: registered_devices +ou: fido2_authenticate dn: ou=metric,o=jans objectClass: top diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json index bfc3e5ec82b..10dbae6ca8f 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json @@ -14,7 +14,6 @@ "scripts": "ou=scripts,o=jans", "umaBase":"ou=uma,o=jans", "umaPolicy":"ou=policies,ou=uma,o=jans", - "u2fBase":"ou=u2f,o=jans", "metric":"ou=statistic,o=metric", "sectorIdentifiers": "ou=sector_identifiers,o=jans", "ciba": "ou=ciba,o=jans", diff --git a/jans-linux-setup/jans_setup/templates/jans-fido2/dynamic-conf.json b/jans-linux-setup/jans_setup/templates/jans-fido2/dynamic-conf.json index 8ce8e90efcd..796ac376abe 100644 --- a/jans-linux-setup/jans_setup/templates/jans-fido2/dynamic-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-fido2/dynamic-conf.json @@ -15,6 +15,8 @@ "jansCustomPerson", "jansPerson" ], + "superGluuEnabled": false, + "oldU2fMigrationEnabled": true, "fido2Configuration":{ "authenticatorCertsFolder":"%(fido2ConfigFolder)s/authenticator_cert", "mdsCertsFolder":"%(fido2ConfigFolder)s/mds/cert", diff --git a/jans-linux-setup/jans_setup/templates/jans-fido2/static-conf.json b/jans-linux-setup/jans_setup/templates/jans-fido2/static-conf.json index c2c23a0a643..2b31aad7d63 100644 --- a/jans-linux-setup/jans_setup/templates/jans-fido2/static-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-fido2/static-conf.json @@ -3,7 +3,10 @@ "configuration":"ou=configuration,o=jans", "people":"ou=people,o=jans", "scripts": "ou=scripts,o=jans", - "metric":"ou=statistic,o=metric", - "sessions":"ou=sessions,o=jans" + "attributes":"ou=attributes,o=jans", + "sessions":"ou=sessions,o=jans", + "fido2Attestation":"ou=fido2_register,ou=fido2,o=jans", + "fido2Assertion":"ou=fido2_auth,ou=fido2,o=jans", + "metric":"ou=statistic,o=metric" } } diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationData.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationData.java similarity index 81% rename from jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationData.java rename to jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationData.java index dfeb86cf993..da2e9d92c12 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationData.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationData.java @@ -4,12 +4,9 @@ * Copyright (c) 2020, Janssen Project */ -package io.jans.fido2.model.entry; - -import io.jans.fido2.ctap.UserVerification; +package io.jans.orm.model.fido2; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import io.jans.orm.model.fido2.Fido2Data; @JsonIgnoreProperties(ignoreUnknown = true) public class Fido2AuthenticationData extends Fido2Data { @@ -29,6 +26,8 @@ public class Fido2AuthenticationData extends Fido2Data { private Fido2AuthenticationStatus status; + private String applicationId; + public String getId() { return id; } @@ -101,11 +100,20 @@ public void setStatus(Fido2AuthenticationStatus status) { this.status = status; } + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + @Override public String toString() { - return "Fido2AuthenticationData [id=" + id + ", username=" + username + ", domain=" + domain - + ", userId=" + userId + ", challenge=" + challenge + ", assertionRequest=" + assertionRequest + ", assertionResponse=" - + assertionResponse + ", userVerificationOption=" + userVerificationOption + ", status=" + status + "]"; + return "Fido2AuthenticationData [id=" + id + ", username=" + username + ", domain=" + domain + ", userId=" + + userId + ", challenge=" + challenge + ", assertionRequest=" + assertionRequest + + ", assertionResponse=" + assertionResponse + ", userVerificationOption=" + userVerificationOption + + ", status=" + status + ", applicationId=" + applicationId + "]"; } } diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationEntry.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationEntry.java similarity index 83% rename from jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationEntry.java rename to jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationEntry.java index 2d1dc0857bf..bef81da2cca 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationEntry.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationEntry.java @@ -4,7 +4,7 @@ * Copyright (c) 2020, Janssen Project */ -package io.jans.fido2.model.entry; +package io.jans.orm.model.fido2; import java.io.Serializable; import java.util.Date; @@ -12,7 +12,6 @@ import io.jans.orm.annotation.AttributeName; import io.jans.orm.annotation.JsonObject; import io.jans.orm.annotation.ObjectClass; -import io.jans.orm.model.fido2.Fido2Entry; /** * Fido2 registration entry @@ -57,10 +56,9 @@ public void setAuthenticationStatus(Fido2AuthenticationStatus authenticationStat this.authenticationStatus = authenticationStatus; } - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("Fido2AuthenticationEntry [authenticationData=").append(authenticationData).append("]"); - return builder.toString(); - } + @Override + public String toString() { + return "Fido2AuthenticationEntry [authenticationData=" + authenticationData + ", authenticationStatus=" + + authenticationStatus + "]"; + } } diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationStatus.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationStatus.java similarity index 97% rename from jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationStatus.java rename to jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationStatus.java index e502e402da4..b69869091f7 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/entry/Fido2AuthenticationStatus.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2AuthenticationStatus.java @@ -4,7 +4,7 @@ * Copyright (c) 2020, Janssen Project */ -package io.jans.fido2.model.entry; +package io.jans.orm.model.fido2; import java.util.HashMap; import java.util.Map; diff --git a/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2DeviceData.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2DeviceData.java new file mode 100644 index 00000000000..a9cd5f09750 --- /dev/null +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2DeviceData.java @@ -0,0 +1,101 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.orm.model.fido2; + +import java.io.Serializable; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * FIDO2 U2F device data + * + * @author Yuriy Movchan Date: 02/16/2016 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Fido2DeviceData implements Serializable { + + private static final long serialVersionUID = -8173244116167488365L; + + @JsonProperty(value = "uuid") + private final String uuid; + + @JsonProperty(value = "push_token") + private final String pushToken; + + @JsonProperty(value = "type") + private final String type; + + @JsonProperty(value = "platform") + private final String platform; + + @JsonProperty(value = "name") + private final String name; + + @JsonProperty(value = "os_name") + private final String osName; + + @JsonProperty(value = "os_version") + private final String osVersion; + + @JsonProperty(value = "custom_data") + private final Map customData; + + public Fido2DeviceData(@JsonProperty(value = "uuid") String uuid, @JsonProperty(value = "token") String pushToken, + @JsonProperty(value = "type") String type, @JsonProperty(value = "platform") String platform, + @JsonProperty(value = "name") String name, @JsonProperty(value = "os_name") String osName, + @JsonProperty(value = "os_version") String osVersion, @JsonProperty(value = "custom_data") Map customData) { + this.uuid = uuid; + this.pushToken = pushToken; + this.type = type; + this.platform = platform; + this.name = name; + this.osName = osName; + this.osVersion = osVersion; + this.customData = customData; + } + + public String getUuid() { + return uuid; + } + + public String getPushToken() { + return pushToken; + } + + public String getType() { + return type; + } + + public String getPlatform() { + return platform; + } + + public String getName() { + return name; + } + + public String getOsName() { + return osName; + } + + public String getOsVersion() { + return osVersion; + } + + public final Map getCustomData() { + return customData; + } + + @Override + public String toString() { + return "DeviceData [uuid=" + uuid + ", pushToken=" + pushToken + ", type=" + type + ", platform=" + platform + ", name=" + name + ", osName=" + + osName + ", osVersion=" + osVersion + ", customData=" + customData + "]"; + } + +} diff --git a/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2Entry.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2Entry.java index 2c289298693..45bdfb23aa8 100644 --- a/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2Entry.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2Entry.java @@ -6,10 +6,14 @@ package io.jans.orm.model.fido2; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; import io.jans.orm.annotation.AttributeName; import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.Expiration; import io.jans.orm.annotation.ObjectClass; import io.jans.orm.model.base.BaseEntry; @@ -23,18 +27,38 @@ @ObjectClass public class Fido2Entry extends BaseEntry { - @AttributeName(ignoreDuringUpdate = true, name = "jansId") + private static final long serialVersionUID = -3122430771066187529L; + + @AttributeName(ignoreDuringUpdate = true, name = "jansId") private String id; @AttributeName(name = "jansCodeChallenge") private String challange; + @AttributeName(name = "jansCodeChallengeHash") + private String challengeHash; + @AttributeName(name = "creationDate") private Date creationDate; @AttributeName(name = "personInum") private String userInum; + @AttributeName(name = "jansApp") + private String rpId; + + @AttributeName(name = "jansSessStateId") + private String sessionStateId; + + @AttributeName(name = "exp") + private Date expirationDate; + + @AttributeName(name = "del") + private boolean deletable = true; + + @Expiration + private Integer ttl; + public Fido2Entry() { } @@ -81,4 +105,71 @@ public String getUserInum() { public void setUserInum(String userInum) { this.userInum = userInum; } + + public String getRpId() { + return rpId; + } + + public void setRpId(String rpId) { + this.rpId = rpId; + } + + public String getSessionStateId() { + return sessionStateId; + } + + public void setSessionStateId(String sessionStateId) { + this.sessionStateId = sessionStateId; + } + + public String getChallengeHash() { + return challengeHash; + } + + public void setChallengeHash(String challengeHash) { + this.challengeHash = challengeHash; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public void clearExpiration() { + this.expirationDate = null; + this.deletable = false; + this.ttl = 0; + } + + public void setExpiration() { + if (creationDate != null) { + final int expiration = 90; + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.setTime(creationDate); + calendar.add(Calendar.SECOND, expiration); + this.expirationDate = calendar.getTime(); + this.deletable = true; + this.ttl = expiration; + } + } + } diff --git a/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationData.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationData.java index 11b0a55edaf..c387c1ab60e 100644 --- a/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationData.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationData.java @@ -33,7 +33,7 @@ public class Fido2RegistrationData extends Fido2Data { private String attestationType; private int signatureAlgorithm; - + private String applicationId; public String getUsername() { diff --git a/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationEntry.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationEntry.java index c1a91cc056c..f625315dba9 100644 --- a/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationEntry.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/Fido2RegistrationEntry.java @@ -24,11 +24,14 @@ public class Fido2RegistrationEntry extends Fido2Entry implements Serializable { private static final long serialVersionUID = -2242931562244920584L; + @AttributeName(name = "displayName") + private String displayName; + @AttributeName(name = "jansPublicKeyId") protected String publicKeyId; - @AttributeName(name = "displayName") - private String displayName; + @AttributeName(name = "jansPublicKeyIdHash") + private Integer publicKeyIdHash; @JsonObject @AttributeName(name = "jansRegistrationData") @@ -44,8 +47,9 @@ public class Fido2RegistrationEntry extends Fido2Entry implements Serializable { @AttributeName(name = "jansDeviceNotificationConf") private String deviceNotificationConf; - @AttributeName(name = "jansCodeChallengeHash") - private String challangeHash; + @JsonObject + @AttributeName(name = "jansDeviceData") + private Fido2DeviceData deviceData; public Fido2RegistrationEntry() { } @@ -88,7 +92,7 @@ public void setRegistrationStatus(Fido2RegistrationStatus registrationStatus) { this.registrationStatus = registrationStatus; } - public String getDeviceNotificationConf() { + public String getDeviceNotificationConf() { return deviceNotificationConf; } @@ -104,19 +108,28 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } - public String getChallangeHash() { - return challangeHash; - } + public Integer getPublicKeyIdHash() { + return publicKeyIdHash; + } - public void setChallangeHash(String challangeHash) { - this.challangeHash = challangeHash; - } + public void setPublicKeyIdHash(Integer publicKeyIdHash) { + this.publicKeyIdHash = publicKeyIdHash; + } - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("Fido2RegistrationEntry [publicKeyId=").append(publicKeyId).append(", registrationData=").append(registrationData).append("]"); - return builder.toString(); - } + public Fido2DeviceData getDeviceData() { + return deviceData; + } + + public void setDeviceData(Fido2DeviceData deviceData) { + this.deviceData = deviceData; + } + + @Override + public String toString() { + return "Fido2RegistrationEntry [displayName=" + displayName + ", publicKeyId=" + publicKeyId + + ", publicKeyIdHash=" + publicKeyIdHash + ", registrationData=" + registrationData + ", counter=" + + counter + ", registrationStatus=" + registrationStatus + ", deviceNotificationConf=" + + deviceNotificationConf + ", deviceData=" + deviceData + "]"; + } } diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/ctap/UserVerification.java b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/UserVerification.java similarity index 88% rename from jans-fido2/model/src/main/java/io/jans/fido2/ctap/UserVerification.java rename to jans-orm/model/src/main/java/io/jans/orm/model/fido2/UserVerification.java index 54c798f7c13..63b3ebc8c25 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/ctap/UserVerification.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/fido2/UserVerification.java @@ -4,7 +4,7 @@ * Copyright (c) 2020, Janssen Project */ -package io.jans.fido2.ctap; +package io.jans.orm.model.fido2; public enum UserVerification {