diff --git a/docs/script-catalog/person_authentication/casa/Casa.py b/docs/script-catalog/person_authentication/casa/Casa.py index f12f479e9f6..77bb67b69b6 100644 --- a/docs/script-catalog/person_authentication/casa/Casa.py +++ b/docs/script-catalog/person_authentication/casa/Casa.py @@ -1,135 +1,660 @@ -# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. -# Copyright (c) 2018, Janssen -# -# Author: Jose Gonzalez - -from io.jans.model.custom.script.type.client import ClientRegistrationType +from io.jans.as.persistence.model.configuration import GluuConfiguration +from io.jans.as.server.security import Identity +from io.jans.as.server.service import AuthenticationService +from io.jans.as.server.service import UserService +from io.jans.as.common.service.common import EncryptionService +from io.jans.as.server.service.custom import CustomScriptService +from io.jans.as.server.service.net import HttpService +from io.jans.as.server.util import ServerUtil +from io.jans.model import SimpleCustomProperty +from io.jans.model.custom.script import CustomScriptType +from io.jans.model.custom.script.type.auth import PersonAuthenticationType +from io.jans.orm import PersistenceEntryManager +from io.jans.service import CacheService from io.jans.service.cdi.util import CdiUtil -from io.jans.as.service import ScopeService -from io.jans.util import StringHelper, ArrayHelper -from java.util import Arrays, ArrayList, HashSet, Date, GregorianCalendar +from io.jans.util import StringHelper + +from java.lang import Integer +from java.util import Collections, HashMap, HashSet, ArrayList, Arrays, Date +from java.nio.charset import Charset + +from org.gluu.casa.model import ApplicationConfiguration +from org.apache.http.params import CoreConnectionPNames -import java +try: + import json +except ImportError: + import simplejson as json +import sys + +class PersonAuthentication(PersonAuthenticationType): -class ClientRegistration(ClientRegistrationType): def __init__(self, currentTimeMillis): self.currentTimeMillis = currentTimeMillis + self.ACR_SG = "super_gluu" + + self.modulePrefix = "casa-external_" def init(self, customScript, configurationAttributes): - print "Casa client registration. Initialization" - self.clientRedirectUrisSet = self.prepareClientRedirectUris(configurationAttributes) - print "Casa client registration. Initialized successfully" - return True - def destroy(self, configurationAttributes): - print "Casa client registration. Destroy" - print "Casa client registration. Destroyed successfully" - return True - - # Update client entry before persistent it - # context refers to io.jans.as.server.service.external.context.DynamicClientRegistrationContext - see https://github.com/JanssenProject/jans-auth-server/blob/e083818272ac48813eca8525e94f7bd73a7a9f1b/server/src/main/java/io/jans/as/server/service/external/context/DynamicClientRegistrationContext.java#L24 - def createClient(self, context): - registerRequest = context.getRegisterRequest() - configurationAttributes = context.getConfigurationAttibutes() - client = context.getClient() - - print "Casa client registration. CreateClient method" - redirectUris = client.getRedirectUris() - print "Casa client registration. Redirect Uris: %s" % redirectUris - - credManagerClient = False - for redirectUri in redirectUris: - if self.clientRedirectUrisSet.contains(redirectUri): - credManagerClient = True - break - - if not credManagerClient: - return True + print "Casa. init called" + self.authenticators = {} + self.uid_attr = self.getLocalPrimaryKey() + + custScriptService = CdiUtil.bean(CustomScriptService) + self.scriptsList = custScriptService.findCustomScripts(Collections.singletonList(CustomScriptType.PERSON_AUTHENTICATION), "jansConfProperty", "displayName", "jansEnabled", "jansLevel") + dynamicMethods = self.computeMethods(self.scriptsList) + + if len(dynamicMethods) > 0: + print "Casa. init. Loading scripts for dynamic modules: %s" % dynamicMethods - print "Casa client registration. Client is Janssen Casa" - self.setClientScopes(client, configurationAttributes.get("scopes")) - #Extend client lifetime for one year - cal=GregorianCalendar() - cal.add(1,10) - client.setClientSecretExpiresAt(Date(cal.getTimeInMillis())) - client.setTrustedClient(True) + for acr in dynamicMethods: + moduleName = self.modulePrefix + acr + try: + external = __import__(moduleName, globals(), locals(), ["PersonAuthentication"], -1) + module = external.PersonAuthentication(self.currentTimeMillis) + + print "Casa. init. Got dynamic module for acr %s" % acr + configAttrs = self.getConfigurationAttributes(acr, self.scriptsList) + + if acr == self.ACR_SG: + application_id = configurationAttributes.get("supergluu_app_id").getValue2() + configAttrs.put("application_id", SimpleCustomProperty("application_id", application_id)) + + if module.init(None, configAttrs): + module.configAttrs = configAttrs + self.authenticators[acr] = module + else: + print "Casa. init. Call to init in module '%s' returned False" % moduleName + except: + print "Casa. init. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + + mobile_methods = configurationAttributes.get("mobile_methods") + self.mobile_methods = [] if mobile_methods == None else StringHelper.split(mobile_methods.getValue2(), ",") + + print "Casa. init. Initialized successfully" return True - # Update client entry before persistent it - # context refers to io.jans.as.server.service.external.context.DynamicClientRegistrationContext - see https://github.com/JanssenProject/jans-auth-server/blob/e083818272ac48813eca8525e94f7bd73a7a9f1b/server/src/main/java/io/jans/as/server/service/external/context/DynamicClientRegistrationContext.java#L24 - def updateClient(self, context): - registerRequest = context.getRegisterRequest() - configurationAttributes = context.getConfigurationAttibutes() - client = context.getClient() - print "Casa client registration. UpdateClient method" - self.setClientScopes(client, configurationAttributes.get("scopes")) + def destroy(self, configurationAttributes): + print "Casa. Destroyed called" return True + def getApiVersion(self): return 11 - # cert - java.security.cert.X509Certificate - # context refers to io.jans.as.server.service.external.context.DynamicClientRegistrationContext - see https://github.com/JanssenProject/jans-auth-server/blob/e083818272ac48813eca8525e94f7bd73a7a9f1b/server/src/main/java/io/jans/as/server/service/external/context/DynamicClientRegistrationContext.java#L24 - def isCertValidForClient(self, cert, context): + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + print "Casa. isValidAuthenticationMethod called" + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Casa. authenticate for step %s" % str(step) + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + if step == 1: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + + foundUser = userService.getUserByAttribute(self.uid_attr, user_name) + #foundUser = userService.getUser(user_name) + if foundUser == None: + print "Casa. authenticate for step 1. Unknown username" + else: + platform_data = self.parsePlatformData(requestParameters) + preferred = foundUser.getAttribute("jansPreferredMethod") + mfaOff = preferred == None + logged_in = False + + if mfaOff: + logged_in = authenticationService.authenticate(user_name, user_password) + else: + acr = self.getSuitableAcr(foundUser, platform_data, preferred) + if acr != None: + module = self.authenticators[acr] + logged_in = module.authenticate(module.configAttrs, requestParameters, step) + + if logged_in: + foundUser = authenticationService.getAuthenticatedUser() + + if foundUser == None: + print "Casa. authenticate for step 1. Cannot retrieve logged user" + else: + if mfaOff: + identity.setWorkingParameter("skip2FA", True) + else: + #Determine whether to skip 2FA based on policy defined (global or user custom) + skip2FA = self.determineSkip2FA(userService, identity, foundUser, platform_data) + identity.setWorkingParameter("skip2FA", skip2FA) + identity.setWorkingParameter("ACR", acr) + + return True + + else: + print "Casa. authenticate for step 1 was not successful" + return False + + else: + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Casa. authenticate for step 2. Cannot retrieve logged user" + return False + + #see casa.xhtml + alter = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if alter != None: + #bypass the rest of this step if an alternative method was provided. Current step will be retried (see getNextStep) + self.simulateFirstStep(requestParameters, alter) + return True + + session_attributes = identity.getSessionId().getSessionAttributes() + acr = session_attributes.get("ACR") + #this working parameter is used in casa.xhtml + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + success = False + if acr in self.authenticators: + module = self.authenticators[acr] + success = module.authenticate(module.configAttrs, requestParameters, step) + + #Update the list of trusted devices if 2fa passed + if success: + print "Casa. authenticate. 2FA authentication was successful" + tdi = session_attributes.get("trustedDevicesInfo") + if tdi == None: + print "Casa. authenticate. List of user's trusted devices was not updated" + else: + user.setAttribute("jansTrustedDevices", tdi) + userService.updateUser(user) + else: + print "Casa. authenticate. 2FA authentication failed" + + return success + return False - def setClientScopes(self, client, requiredScopes): - - if requiredScopes == None: - print "Casa client registration. No list of scopes was passed in script parameters" - return - - requiredScopes = StringHelper.split(requiredScopes.getValue2(), ",") - newScopes = client.getScopes() - scopeService = CdiUtil.bean(ScopeService) - - for scopeName in requiredScopes: - scope = scopeService.getScopeById(scopeName) - if not scope.isDefaultScope(): - print "Casa client registration. Adding scope '%s'" % scopeName - newScopes = ArrayHelper.addItemToStringArray(newScopes, scope.getDn()) - - print "Casa client registration. Result scopes are: %s" % newScopes - client.setScopes(newScopes) + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Casa. prepareForStep %s" % str(step) + identity = CdiUtil.bean(Identity) + + if step == 1: + self.prepareUIParams(identity) + return True + else: + session_attributes = identity.getSessionId().getSessionAttributes() + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + + if user == None: + print "Casa. prepareForStep. Cannot retrieve logged user" + return False + + acr = session_attributes.get("ACR") + print "Casa. prepareForStep. ACR = %s" % acr + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + if acr in self.authenticators: + module = self.authenticators[acr] + return module.prepareForStep(module.configAttrs, requestParameters, step) + else: + return False + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Casa. getExtraParametersForStep %s" % str(step) + list = ArrayList() + + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + + if acr in self.authenticators: + module = self.authenticators[acr] + params = module.getExtraParametersForStep(module.configAttrs, step) + if params != None: + list.addAll(params) + + list.addAll(Arrays.asList("ACR", "methods", "trustedDevicesInfo")) + + list.addAll(Arrays.asList("casa_contextPath", "casa_prefix", "casa_faviconUrl", "casa_extraCss", "casa_logoUrl")) + print "extras are %s" % list + return list + + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Casa. getCountAuthenticationSteps called" + + if CdiUtil.bean(Identity).getWorkingParameter("skip2FA"): + return 1 + + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + return module.getCountAuthenticationSteps(module.configAttrs) + else: + return 2 + + print "Casa. getCountAuthenticationSteps. Could not determine the step count for acr %s" % acr + + + def getPageForStep(self, configurationAttributes, step): + print "Casa. getPageForStep called %s" % str(step) + + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + page = module.getPageForStep(module.configAttrs, step) + else: + page=None + + return page + + return "/casa/login.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + + print "Casa. getNextStep called %s" % str(step) + if step > 1: + acr = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if acr != None: + print "Casa. getNextStep. Use alternative method %s" % acr + CdiUtil.bean(Identity).setWorkingParameter("ACR", acr) + #retry step with different acr + return 2 + + return -1 + + + def logout(self, configurationAttributes, requestParameters): + print "Casa. logout called" + return True + +# Miscelaneous + + def getLocalPrimaryKey(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = GluuConfiguration() + config = entryManager.find(config.getClass(), "ou=configuration,o=jans") + #Pick (one) attribute where user id is stored (e.g. uid/mail) + # primaryKey is the primary key on the backend AD / LDAP Server + # localPrimaryKey is the primary key on Gluu. This attr value has been mapped with the primary key attr of the backend AD / LDAP when configuring cache refresh + uid_attr = config.getIdpAuthn().get(0).getConfig().findValue("localPrimaryKey").asText() + print "Casa. init. uid attribute is '%s'" % uid_attr + return uid_attr + + + def getSettings(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = ApplicationConfiguration() + config = entryManager.find(config.getClass(), "ou=casa,ou=configuration,o=jans") + settings = config.getSettings() + if settings == None: + print "Casa. getSettings. Failed to parse casa settings from DB" + return settings + + + def computeMethods(self, scriptList): + + methods = [] + mapping = {} + cmConfigs = self.getSettings() + + if cmConfigs != None and cmConfigs.getAcrPluginMap() != None: + mapping = cmConfigs.getAcrPluginMap().keySet() + + for m in mapping: + for customScript in scriptList: + if customScript.getName() == m and customScript.isEnabled(): + methods.append(m) + + print "Casa. computeMethods. %s" % methods + return methods + + + def getConfigurationAttributes(self, acr, scriptsList): + + configMap = HashMap() + for customScript in scriptsList: + if customScript.getName() == acr and customScript.isEnabled(): + for prop in customScript.getConfigurationProperties(): + configMap.put(prop.getValue1(), SimpleCustomProperty(prop.getValue1(), prop.getValue2())) + + print "Casa. getConfigurationAttributes. %d configuration properties were found for %s" % (configMap.size(), acr) + return configMap + + + def getAvailMethodsUser(self, user, skip=None): + methods = HashSet() + + for method in self.authenticators: + try: + module = self.authenticators[method] + if module.hasEnrollments(module.configAttrs, user): + methods.add(method) + except: + print "Casa. getAvailMethodsUser. hasEnrollments call could not be issued for %s module" % method + print "Exception: ", sys.exc_info()[1] + + try: + if skip != None: + # skip is guaranteed to be a member of methods (if hasEnrollments routines are properly implemented). + # A call to remove strangely crashes when skip is absent + methods.remove(skip) + except: + print "Casa. getAvailMethodsUser. methods list does not contain %s" % skip + + print "Casa. getAvailMethodsUser %s" % methods.toString() + return methods + + + def prepareUIParams(self, identity): - def prepareClientRedirectUris(self, configurationAttributes): - clientRedirectUrisSet = HashSet() - if not configurationAttributes.containsKey("client_redirect_uris"): - return clientRedirectUrisSet - - clientRedirectUrisList = configurationAttributes.get("client_redirect_uris").getValue2() - if StringHelper.isEmpty(clientRedirectUrisList): - print "Casa client registration. The property client_redirect_uris is empty" - return clientRedirectUrisSet - - clientRedirectUrisArray = StringHelper.split(clientRedirectUrisList, ",") - if ArrayHelper.isEmpty(clientRedirectUrisArray): - print "Casa client registration. No clients specified in client_redirect_uris property" - return clientRedirectUrisSet + print "Casa. prepareUIParams. Reading UI branding params" + cacheService = CdiUtil.bean(CacheService) + casaAssets = cacheService.get("casa_assets") + + if casaAssets == None: + #This may happen when cache type is IN_MEMORY, where actual cache is merely a local variable + #(a expiring map) living inside Casa webapp, not oxAuth webapp + + sets = self.getSettings() + + custPrefix = "/custom" + logoUrl = "/images/logo.png" + faviconUrl = "/images/favicon.ico" + if (sets.getExtraCssSnippet() != None) or sets.isUseExternalBranding(): + logoUrl = custPrefix + logoUrl + faviconUrl = custPrefix + faviconUrl + + prefix = custPrefix if sets.isUseExternalBranding() else "" + + casaAssets = { + "contextPath": "/casa", + "prefix" : prefix, + "faviconUrl" : faviconUrl, + "extraCss": sets.getExtraCssSnippet(), + "logoUrl": logoUrl + } - # Convert to HashSet to quick search - i = 0 - count = len(clientRedirectUrisArray) - while i < count: - uris = clientRedirectUrisArray[i] - clientRedirectUrisSet.add(uris) - i = i + 1 - - return clientRedirectUrisSet - - # responseAsJsonObject - is org.json.JSONObject, you can use any method to manipulate json - # context is reference of io.jans.as.server.model.common.ExecutionContext - def modifyPutResponse(self, responseAsJsonObject, executionContext): - return False + #Setting a single variable with the whole map does not work... + identity.setWorkingParameter("casa_contextPath", casaAssets['contextPath']) + identity.setWorkingParameter("casa_prefix", casaAssets['prefix']) + identity.setWorkingParameter("casa_faviconUrl", casaAssets['contextPath'] + casaAssets['faviconUrl']) + identity.setWorkingParameter("casa_extraCss", casaAssets['extraCss']) + identity.setWorkingParameter("casa_logoUrl", casaAssets['contextPath'] + casaAssets['logoUrl']) - # responseAsJsonObject - is org.json.JSONObject, you can use any method to manipulate json - # context is reference of io.jans.as.server.model.common.ExecutionContext - def modifyReadResponse(self, responseAsJsonObject, executionContext): - return False - # responseAsJsonObject - is org.json.JSONObject, you can use any method to manipulate json - # context is reference of io.jans.as.server.model.common.ExecutionContext - def modifyPostResponse(self, responseAsJsonObject, executionContext): - return False + def simulateFirstStep(self, requestParameters, acr): + #To simulate 1st step, there is no need to call: + # getPageforstep (no need as user/pwd won't be shown again) + # isValidAuthenticationMethod (by restriction, it returns True) + # prepareForStep (by restriction, it returns True) + # getExtraParametersForStep (by restriction, it returns None) + print "Casa. simulateFirstStep. Calling authenticate (step 1) for %s module" % acr + if acr in self.authenticators: + module = self.authenticators[acr] + auth = module.authenticate(module.configAttrs, requestParameters, 1) + print "Casa. simulateFirstStep. returned value was %s" % auth + + +# 2FA policy enforcement + + def parsePlatformData(self, requestParameters): + try: + #Find device info passed in HTTP request params (see index.xhtml) + platform = ServerUtil.getFirstValue(requestParameters, "loginForm:platform") + deviceInf = json.loads(platform) + except: + print "Casa. parsePlatformData. Error parsing platform data" + deviceInf = None + + return deviceInf + + + def getSuitableAcr(self, user, deviceInf, preferred): + + onMobile = deviceInf != None and 'isMobile' in deviceInf and deviceInf['isMobile'] + id = user.getUserId() + strongest = -1 + acr = None + user_methods = self.getAvailMethodsUser(user) + + for s in self.scriptsList: + name = s.getName() + level = Integer.MAX_VALUE if name == preferred else s.getLevel() + if user_methods.contains(name) and level > strongest and (not onMobile or name in self.mobile_methods): + acr = name + strongest = level + + print "Casa. getSuitableAcr. On mobile = %s" % onMobile + if acr == None and onMobile: + print "Casa. getSuitableAcr. No mobile-friendly authentication method available for user %s" % id + # user_methods is not empty when this function is called, so just pick any + acr = user_methods.stream().findFirst().get() + + print "Casa. getSuitableAcr. %s was selected for user %s" % (acr, id) + return acr + + + def determineSkip2FA(self, userService, identity, foundUser, deviceInf): + + cmConfigs = self.getSettings() + + if cmConfigs == None: + print "Casa. determineSkip2FA. Failed to read policy_2fa" + return False + + cmConfigs = cmConfigs.getPluginSettings().get('strong-authn-settings') + + policy2FA = 'EVERY_LOGIN' + if cmConfigs != None and cmConfigs.get('policy_2fa') != None: + policy2FA = ','.join(cmConfigs.get('policy_2fa')) + + print "Casa. determineSkip2FA with general policy %s" % policy2FA + policy2FA += ',' + skip2FA = False + + if 'CUSTOM,' in policy2FA: + #read setting from user profile + policy = foundUser.getAttribute("jansStrongAuthPolicy") + if policy == None: + policy = 'EVERY_LOGIN,' + else: + policy = policy.upper() + ',' + print "Casa. determineSkip2FA. Using user's enforcement policy %s" % policy + + else: + #If it's not custom, then apply the global setting admin defined + policy = policy2FA + + if not 'EVERY_LOGIN,' in policy: + locationCriterion = 'LOCATION_UNKNOWN,' in policy + deviceCriterion = 'DEVICE_UNKNOWN,' in policy + + if locationCriterion or deviceCriterion: + if deviceInf == None: + print "Casa. determineSkip2FA. No user device data. Forcing 2FA to take place..." + else: + skip2FA = self.process2FAPolicy(identity, foundUser, deviceInf, locationCriterion, deviceCriterion) + + if skip2FA: + print "Casa. determineSkip2FA. Second factor is skipped" + #Update attribute if authentication will not have second step + devInf = identity.getWorkingParameter("trustedDevicesInfo") + if devInf != None: + foundUser.setAttribute("jansTrustedDevices", devInf) + userService.updateUser(foundUser) + else: + print "Casa. determineSkip2FA. Unknown %s policy: cannot skip 2FA" % policy + + return skip2FA + + + def process2FAPolicy(self, identity, foundUser, deviceInf, locationCriterion, deviceCriterion): + + skip2FA = False + #Retrieve user's devices info + devicesInfo = foundUser.getAttribute("jansTrustedDevices") + + #do geolocation + geodata = self.getGeolocation(identity) + if geodata == None: + print "Casa. process2FAPolicy: Geolocation data not obtained. 2FA skipping based on location cannot take place" + + try: + encService = CdiUtil.bean(EncryptionService) + + if devicesInfo == None: + print "Casa. process2FAPolicy: There are no trusted devices for user yet" + #Simulate empty list + devicesInfo = "[]" + else: + devicesInfo = encService.decrypt(devicesInfo) + + devicesInfo = json.loads(devicesInfo) + + partialMatch = False + idx = 0 + #Try to find a match for device only + for device in devicesInfo: + partialMatch = device['browser']['name']==deviceInf['name'] and device['os']['version']==deviceInf['os']['version'] and device['os']['family']==deviceInf['os']['family'] + if partialMatch: + break + idx+=1 + + matchFound = False + + #At least one of locationCriterion or deviceCriterion is True + if locationCriterion and not deviceCriterion: + #this check makes sense if there is city data only + if geodata!=None: + for device in devicesInfo: + #Search all registered cities that are found in trusted devices + for origin in device['origins']: + matchFound = matchFound or origin['city']==geodata['city'] + + elif partialMatch: + #In this branch deviceCriterion is True + if not locationCriterion: + matchFound = True + elif geodata!=None: + for origin in devicesInfo[idx]['origins']: + matchFound = matchFound or origin['city']==geodata['city'] + + skip2FA = matchFound + now = Date().getTime() + + #Update attribute oxTrustedDevicesInfo accordingly + if partialMatch: + #Update an existing record (update timestamp in city, or else add it) + if geodata != None: + partialMatch = False + idxCity = 0 + + for origin in devicesInfo[idx]['origins']: + partialMatch = origin['city']==geodata['city'] + if partialMatch: + break; + idxCity+=1 + + if partialMatch: + devicesInfo[idx]['origins'][idxCity]['timestamp'] = now + else: + devicesInfo[idx]['origins'].append({"city": geodata['city'], "country": geodata['country'], "timestamp": now}) + else: + #Create a new entry + browser = {"name": deviceInf['name'], "version": deviceInf['version']} + os = {"family": deviceInf['os']['family'], "version": deviceInf['os']['version']} + + if geodata == None: + origins = [] + else: + origins = [{"city": geodata['city'], "country": geodata['country'], "timestamp": now}] + + obj = {"browser": browser, "os": os, "addedOn": now, "origins": origins} + devicesInfo.append(obj) + + enc = json.dumps(devicesInfo, separators=(',',':')) + enc = encService.encrypt(enc) + identity.setWorkingParameter("trustedDevicesInfo", enc) + + except: + print "Casa. process2FAPolicy. Error!", sys.exc_info()[1] + + return skip2FA + + + def getGeolocation(self, identity): + + session_attributes = identity.getSessionId().getSessionAttributes() + if session_attributes.containsKey("remote_ip"): + remote_ip = session_attributes.get("remote_ip").split(",", 2)[0].strip() + if StringHelper.isNotEmpty(remote_ip): + + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 4 * 1000) + + geolocation_service_url = "http://ip-api.com/json/%s?fields=country,city,status,message" % remote_ip + geolocation_service_headers = { "Accept" : "application/json" } + + try: + http_service_response = httpService.executeGet(http_client, geolocation_service_url, geolocation_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "Casa. Determine remote location. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Casa. Determine remote location. Get non 200 OK response from server:", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes, Charset.forName("UTF-8")) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Casa. Determine remote location. Get empty response from location server" + return None + + response = json.loads(response_string) + + if not StringHelper.equalsIgnoreCase(response['status'], "success"): + print "Casa. Determine remote location. Get response with status: '%s'" % response['status'] + return None + + return response + + return None + + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None diff --git a/jans-fido2/server/pom.xml b/jans-fido2/server/pom.xml index e3a5e7b64a6..c40d2de26be 100644 --- a/jans-fido2/server/pom.xml +++ b/jans-fido2/server/pom.xml @@ -14,10 +14,13 @@ fido2-server - + + profiles/default/config-build.properties + src/main/resources + true **/*.xml **/*.properties diff --git a/jans-fido2/server/profiles/default/config-build.properties b/jans-fido2/server/profiles/default/config-build.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java index 436c6e3902d..5a79d79de80 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java @@ -98,6 +98,9 @@ public class AppInitializer { @Inject private LoggerService loggerService; + @Inject + private MDS3UpdateTimer mds3UpdateTimer; + @PostConstruct public void createApplicationComponents() { try { @@ -113,10 +116,12 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas configurationFactory.create(); PersistenceEntryManager localPersistenceEntryManager = persistenceEntryManagerInstance.get(); - log.trace("Attempting to use {}: {}", ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME, localPersistenceEntryManager.getOperationService()); + log.trace("Attempting to use {}: {}", ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME, + localPersistenceEntryManager.getOperationService()); // Initialize python interpreter - pythonService.initPythonInterpreter(configurationFactory.getBaseConfiguration().getString("pythonModulesDir", null)); + pythonService + .initPythonInterpreter(configurationFactory.getBaseConfiguration().getString("pythonModulesDir", null)); // Initialize script manager List supportedCustomScriptTypes = Lists.newArrayList(CustomScriptType.values()); @@ -132,6 +137,7 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas configurationFactory.initTimer(); loggerService.initTimer(); cleanerTimer.initTimer(); + mds3UpdateTimer.initTimer(); customScriptManager.initTimer(supportedCustomScriptTypes); // Notify plugins about finish application initialization diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/MDS3UpdateEvent.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/MDS3UpdateEvent.java new file mode 100644 index 00000000000..09ea2cb6669 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/MDS3UpdateEvent.java @@ -0,0 +1,5 @@ +package io.jans.fido2.service.app; + +public interface MDS3UpdateEvent { + +} \ No newline at end of file diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/MDS3UpdateTimer.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/MDS3UpdateTimer.java new file mode 100644 index 00000000000..10a71c1a3ab --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/MDS3UpdateTimer.java @@ -0,0 +1,74 @@ +/* + * 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.app; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import io.jans.fido2.service.mds.TocService; +import io.jans.service.cdi.async.Asynchronous; +import io.jans.service.cdi.event.Scheduled; +import io.jans.service.timer.event.TimerEvent; +import io.jans.service.timer.schedule.TimerSchedule; +import org.slf4j.Logger; + +/** + * @author madhumitas + * + */ +@ApplicationScoped +@Named + +public class MDS3UpdateTimer { + + private static final int DEFAULT_INTERVAL = 60 * 60 * 24; // every 24 hours + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private TocService tocService; + + public void initTimer() { + log.info("Initializing MDS3 Update Timer"); + + timerEvent.fire(new TimerEvent(new TimerSchedule(DEFAULT_INTERVAL, DEFAULT_INTERVAL), new MDS3UpdateEvent() { + }, Scheduled.Literal.INSTANCE)); + + log.info("Initialized MDS3 Update Timer"); + } + + @Asynchronous + public void process(@Observes @Scheduled MDS3UpdateEvent mds3UpdateEvent) { + LocalDate nextUpdate = tocService.getNextUpdateDate(); + if (nextUpdate.equals(LocalDate.now()) || nextUpdate.isBefore(LocalDate.now())) { + log.info("Downloading the latest TOC from https://mds.fidoalliance.org/"); + try { + tocService.downloadMdsFromServer(new URL("https://mds.fidoalliance.org/")); + + } catch (MalformedURLException e) { + log.error("Error while parsing the FIDO alliance URL :", e); + return; + } + tocService.refresh(); + } else { + log.info("{} more days for MDS3 Update", LocalDate.now().until(nextUpdate, ChronoUnit.DAYS)); + } + } + +} \ No newline at end of file diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/AttestationCertificateService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/AttestationCertificateService.java index 316d9ad8704..dc7ba20152e 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/AttestationCertificateService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/AttestationCertificateService.java @@ -6,6 +6,7 @@ package io.jans.fido2.service.mds; +import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -80,13 +81,23 @@ public void init(@Observes @ApplicationInitialized(ApplicationScoped.class) Obje this.rootCertificatesMap = certificateService.getCertificatesMap(authenticatorCertsFolder); } - public List getAttestationRootCertificates(JsonNode metadataNode, List attestationCertificates) { - if (metadataNode == null || !metadataNode.has("attestationRootCertificates")) { - List selectedRootCertificate = certificateService.selectRootCertificates(rootCertificatesMap, attestationCertificates); + public List getAttestationRootCertificates(JsonNode metadataNode, + List attestationCertificates) { + JsonNode metaDataStatement = null; + try { + metaDataStatement = dataMapperService.readTree(metadataNode.get("metadataStatement").toPrettyString()); + } catch (IOException e) { + log.error("Error parsing the metadata statement", e); + } + + if (metaDataStatement == null || !metaDataStatement.has("attestationRootCertificates")) { + List selectedRootCertificate = certificateService + .selectRootCertificates(rootCertificatesMap, attestationCertificates); + return selectedRootCertificate; } - ArrayNode node = (ArrayNode) metadataNode.get("attestationRootCertificates"); + ArrayNode node = (ArrayNode) metaDataStatement.get("attestationRootCertificates"); Iterator iter = node.elements(); List x509certificates = new ArrayList<>(); while (iter.hasNext()) { @@ -103,12 +114,11 @@ public List getAttestationRootCertificates(AuthData authData, L JsonNode metadataForAuthenticator = localMdsService.getAuthenticatorsMetadata(aaguid); if (metadataForAuthenticator == null) { try { - log.info("No metadata for authenticator {}. Attempting to contact MDS", aaguid); + log.info("No Local metadata for authenticator {}. Checking for metadata MDS3 blob", aaguid); JsonNode metadata = mdsService.fetchMetadata(authData.getAaguid()); commonVerifiers.verifyThatMetadataIsValid(metadata); - metadataForAuthenticator = metadata; - return getAttestationRootCertificates(metadataForAuthenticator, attestationCertificates); + return getAttestationRootCertificates(metadata, attestationCertificates); } catch (Fido2RuntimeException ex) { log.warn("Failed to get metadata from Fido2 meta-data server"); @@ -146,4 +156,4 @@ private KeyStore getCertificationKeyStore(String aaguid, List c return keyStoreCreator.createKeyStore(aaguid, certificates); } -} +} \ No newline at end of file diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/MdsService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/MdsService.java index 705f9eaf9bb..f84a5c56a62 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/MdsService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/MdsService.java @@ -18,10 +18,6 @@ package io.jans.fido2.service.mds; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -29,15 +25,11 @@ import java.util.List; import java.util.Map; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.Response.StatusType; - import org.apache.commons.codec.binary.Hex; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; + import io.jans.fido2.exception.Fido2RuntimeException; import io.jans.fido2.model.conf.AppConfiguration; import io.jans.fido2.model.conf.Fido2Configuration; @@ -47,11 +39,10 @@ import io.jans.fido2.service.client.ResteasyClientFactory; import io.jans.fido2.service.verifier.CommonVerifiers; import io.jans.service.cdi.event.ApplicationInitialized; -import io.jans.util.StringHelper; -import org.jboss.resteasy.client.jaxrs.ResteasyClient; -import org.slf4j.Logger; -import com.fasterxml.jackson.databind.JsonNode; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; @ApplicationScoped public class MdsService { @@ -83,99 +74,51 @@ public void init(@Observes @ApplicationInitialized(ApplicationScoped.class) Obje this.mdsEntries = Collections.synchronizedMap(new HashMap()); } - public JsonNode fetchMetadata(byte[] aaguidBuffer) { - Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration(); - if (fido2Configuration == null) { - throw new Fido2RuntimeException("Fido2 configuration not exists"); - } - - String mdsAccessToken = fido2Configuration.getMdsAccessToken(); - if (StringHelper.isEmpty(mdsAccessToken)) { - throw new Fido2RuntimeException("Fido2 MDS access token should be set"); - } - - String aaguid = deconvert(aaguidBuffer); - - JsonNode mdsEntry = mdsEntries.get(aaguid); - if (mdsEntry != null) { - log.debug("Get MDS by aaguid {} from cache", aaguid); - return mdsEntry; - } - - JsonNode tocEntry = tocService.getAuthenticatorsMetadata(aaguid); - if (tocEntry == null) { - throw new Fido2RuntimeException("Authenticator not in TOC aaguid " + aaguid); - } - - String tocEntryUrl = tocEntry.get("url").asText(); - URI metadataUrl; - try { - metadataUrl = new URI(String.format("%s/?token=%s", tocEntryUrl, mdsAccessToken)); - log.debug("Authenticator AAGUI {} url metadataUrl {} downloaded", aaguid, metadataUrl); - } catch (URISyntaxException e) { - throw new Fido2RuntimeException("Invalid URI in TOC aaguid " + aaguid); - } + public JsonNode fetchMetadata(byte[] aaguidBuffer) { + Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration(); + if (fido2Configuration == null) { + throw new Fido2RuntimeException("Fido2 configuration not exists"); + } - verifyTocEntryStatus(aaguid, tocEntry); - String metadataHash = commonVerifiers.verifyThatFieldString(tocEntry, "hash"); + String aaguid = deconvert(aaguidBuffer); - log.debug("Reaching MDS at {}", tocEntryUrl); + JsonNode mdsEntry = mdsEntries.get(aaguid); + if (mdsEntry != null) { + log.debug("Get MDS by aaguid {} from cache", aaguid); + return mdsEntry; + } - mdsEntry = downloadMdsFromServer(aaguid, metadataUrl, metadataHash); + JsonNode tocEntry = tocService.getAuthenticatorsMetadata(aaguid); + if (tocEntry == null) { + throw new Fido2RuntimeException("Authenticator not in TOC aaguid " + aaguid); + } - mdsEntries.put(aaguid, mdsEntry); - - return mdsEntry; - } + verifyTocEntryStatus(aaguid, tocEntry); - private JsonNode downloadMdsFromServer(String aaguid, URI metadataUrl, String metadataHash) { - ResteasyClient resteasyClient = resteasyClientFactory.buildResteasyClient(); - Response response = resteasyClient.target(metadataUrl).request().header("Content-Type", MediaType.APPLICATION_JSON).get(); - String body = response.readEntity(String.class); - - StatusType status = response.getStatusInfo(); - log.debug("Response from resource server {}", status); - if (status.getFamily() == Status.Family.SUCCESSFUL) { - byte[] bodyBuffer; - try { - bodyBuffer = body.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new Fido2RuntimeException("Unable to verify metadata hash for aaguid " + aaguid); - } - - byte[] digest = tocService.getDigester().digest(bodyBuffer); - if (!Arrays.equals(digest, base64Service.urlDecode(metadataHash))) { - throw new Fido2RuntimeException("Unable to verify metadata hash for aaguid " + aaguid); - } - - try { - return dataMapperService.readTree(base64Service.urlDecode(body)); - } catch (IOException e) { - log.error("Can't parse payload from the server"); - throw new Fido2RuntimeException("Unable to parse payload from server for aaguid " + aaguid); - } - } else { - throw new Fido2RuntimeException("Unable to retrieve metadata for aaguid " + aaguid + " status " + status); - } + return tocEntry; } - private void verifyTocEntryStatus(String aaguid, JsonNode tocEntry) { - JsonNode statusReports = tocEntry.get("statusReports"); - - Iterator iter = statusReports.elements(); - while (iter.hasNext()) { - JsonNode statusReport = iter.next(); - AuthenticatorCertificationStatus authenticatorStatus = AuthenticatorCertificationStatus.valueOf(statusReport.get("status").asText()); - String authenticatorEffectiveDate = statusReport.get("effectiveDate").asText(); - log.debug("Authenticator AAGUI {} status {} effective date {}", aaguid, authenticatorStatus, authenticatorEffectiveDate); - verifyStatusAcceptable(aaguid, authenticatorStatus); - } - } + + + private void verifyTocEntryStatus(String aaguid, JsonNode tocEntry) { + JsonNode statusReports = tocEntry.get("statusReports"); + + Iterator iter = statusReports.elements(); + while (iter.hasNext()) { + JsonNode statusReport = iter.next(); + AuthenticatorCertificationStatus authenticatorStatus = AuthenticatorCertificationStatus + .valueOf(statusReport.get("status").asText()); + String authenticatorEffectiveDate = statusReport.get("effectiveDate").asText(); + log.debug("Authenticator AAGUID {} status {} effective date {}", aaguid, authenticatorStatus, + authenticatorEffectiveDate); + verifyStatusAcceptable(aaguid, authenticatorStatus); + } + } - private String deconvert(byte[] aaguidBuffer) { - return Hex.encodeHexString(aaguidBuffer).replaceFirst("([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]+)", - "$1-$2-$3-$4-$5"); - } + private String deconvert(byte[] aaguidBuffer) { + return Hex.encodeHexString(aaguidBuffer).replaceFirst( + "([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]+)", "$1-$2-$3-$4-$5"); + } private void verifyStatusAcceptable(String aaguid, AuthenticatorCertificationStatus status) { final List undesiredAuthenticatorStatus = Arrays @@ -192,4 +135,4 @@ public void clear() { this.mdsEntries.clear(); } -} +} \ No newline at end of file diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/TocService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/TocService.java index 1c5d3ed958b..9102016be96 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/TocService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/mds/TocService.java @@ -5,9 +5,10 @@ */ package io.jans.fido2.service.mds; - +import java.nio.file.StandardCopyOption; +import java.io.InputStream; import static java.time.format.DateTimeFormatter.ISO_DATE; - +import java.net.URL; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.DirectoryStream; @@ -17,6 +18,8 @@ import java.security.MessageDigest; import java.security.cert.X509Certificate; import java.security.interfaces.ECPublicKey; + +import java.security.interfaces.RSAPublicKey; import java.text.ParseException; import java.time.LocalDate; import java.util.ArrayList; @@ -27,7 +30,7 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; - +import io.jans.fido2.service.client.ResteasyClientFactory; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; @@ -52,11 +55,8 @@ import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.ECDSAVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; -/** - * @author Yuriy Movchan - * @version May 08, 2020 - */ @ApplicationScoped public class TocService { @@ -77,53 +77,69 @@ public class TocService { @Inject private AppConfiguration appConfiguration; + + @Inject + private ResteasyClientFactory resteasyClientFactory; private Map tocEntries; + + private LocalDate nextUpdate; private MessageDigest digester; + + public LocalDate getNextUpdateDate() + { + return nextUpdate; + } public void init(@Observes @ApplicationInitialized(ApplicationScoped.class) Object init) { - this.tocEntries = Collections.synchronizedMap(new HashMap()); - tocEntries.putAll(parseTOCs()); + refresh(); } - private Map parseTOCs() { - Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration(); - if (fido2Configuration == null) { - log.warn("Fido2 configuration not exists"); - return new HashMap(); - } - - String mdsTocRootCertsFolder = fido2Configuration.getMdsCertsFolder(); - String mdsTocFilesFolder = fido2Configuration.getMdsTocsFolder(); - if (StringHelper.isEmpty(mdsTocRootCertsFolder) || StringHelper.isEmpty(mdsTocFilesFolder)) { - log.warn("Fido2 MDS cert and TOC properties should be set"); - return new HashMap(); - } - log.info("Populating TOC entries from {}", mdsTocFilesFolder); - - Path path = FileSystems.getDefault().getPath(mdsTocFilesFolder); - List> maps = new ArrayList<>(); - try (DirectoryStream directoryStream = Files.newDirectoryStream(path)) { - Iterator iter = directoryStream.iterator(); - while (iter.hasNext()) { - Path filePath = iter.next(); - try { - Pair> result = parseTOC(mdsTocRootCertsFolder, filePath); - log.info("Get TOC {} entries with nextUpdate date {}", result.getSecond().size(), result.getFirst()); - - maps.add(result.getSecond()); - } catch (IOException e) { - log.warn("Can't access or open path: {}", filePath, e); - } catch (ParseException e) { - log.warn("Can't parse path: {}", filePath, e); - } - } - } catch (Exception e) { - log.warn("Something wrong with path", e); - } - - return mergeAndResolveDuplicateEntries(maps); + public void refresh() + { + this.tocEntries = Collections.synchronizedMap(new HashMap()); + tocEntries.putAll(parseTOCs()); } + + private Map parseTOCs() { + Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration(); + if (fido2Configuration == null) { + log.warn("Fido2 configuration not exists"); + return new HashMap(); + } + + String mdsTocRootCertsFolder = fido2Configuration.getMdsCertsFolder(); + String mdsTocFilesFolder = fido2Configuration.getMdsTocsFolder(); + if (StringHelper.isEmpty(mdsTocRootCertsFolder) || StringHelper.isEmpty(mdsTocFilesFolder)) { + log.warn("Fido2 MDS cert and TOC properties should be set"); + return new HashMap(); + } + log.info("Populating TOC entries from {}", mdsTocFilesFolder); + + Path path = FileSystems.getDefault().getPath(mdsTocFilesFolder); + List> maps = new ArrayList<>(); + try (DirectoryStream directoryStream = Files.newDirectoryStream(path)) { + Iterator iter = directoryStream.iterator(); + while (iter.hasNext()) { + Path filePath = iter.next(); + try { + Pair> result = parseTOC(mdsTocRootCertsFolder, filePath); + log.info("Get TOC {} entries with nextUpdate date {}", result.getSecond().size(), + result.getFirst()); + + maps.add(result.getSecond()); + } catch (IOException e) { + log.warn("Can't access or open path: {}", filePath, e); + } catch (ParseException e) { + log.warn("Can't parse path: {}", filePath, e); + } + } + } catch (Exception e) { + log.warn("Something wrong with path", e); + } + + return mergeAndResolveDuplicateEntries(maps); + } private Map parseTOC(String mdsTocRootCertFile, String mdsTocFileLocation) { try { @@ -135,75 +151,146 @@ private Map parseTOC(String mdsTocRootCertFile, String mdsTocF } } - private Pair> parseTOC(String mdsTocRootCertsFolder, Path path) throws IOException, ParseException { - try (BufferedReader reader = Files.newBufferedReader(path)) { - JWSObject jwsObject = JWSObject.parse(reader.readLine()); - - List certificateChain = jwsObject.getHeader().getX509CertChain().stream().map(c -> base64Service.encodeToString(c.decode())) - .collect(Collectors.toList()); - JWSAlgorithm algorithm = jwsObject.getHeader().getAlgorithm(); - - try { - JWSVerifier verifier = resolveVerifier(algorithm, mdsTocRootCertsFolder, certificateChain); - if (!jwsObject.verify(verifier)) { - log.warn("Unable to verify JWS object using algorithm {} for file {}", algorithm, path); - return new Pair>(null, Collections.emptyMap()); - } - } catch (Exception e) { - log.warn("Unable to verify JWS object using algorithm {} for file {} {}", algorithm, path, e); - return new Pair>(null, Collections.emptyMap()); - } - - String jwtPayload = jwsObject.getPayload().toString(); - JsonNode toc = dataMapperService.readTree(jwtPayload); - log.debug("Legal header {}", toc.get("legalHeader")); - - ArrayNode entries = (ArrayNode) toc.get("entries"); - int numberOfEntries = toc.get("no").asInt(); - log.debug("Property 'no' value: {}. Number of entries: {}", numberOfEntries, entries.size()); - - Iterator iter = entries.elements(); - Map tocEntries = new HashMap<>(); - while (iter.hasNext()) { - JsonNode tocEntry = iter.next(); - if (tocEntry.hasNonNull("aaguid")) { - String aaguid = tocEntry.get("aaguid").asText(); - log.info("Added TOC entry {} from {} with status {}", aaguid, path, tocEntry.get("statusReports").findValue("status")); - tocEntries.put(aaguid, tocEntry); - } - } - - String nextUpdateText = toc.get("nextUpdate").asText(); - - LocalDate nextUpdateDate = LocalDate.parse(nextUpdateText); - - this.digester = resolveDigester(algorithm); - - return new Pair>(nextUpdateDate, tocEntries); - } - } + private Pair> parseTOC(String mdsTocRootCertsFolder, Path path) + throws IOException, ParseException { + try (BufferedReader reader = Files.newBufferedReader(path)) { + JWSObject jwsObject = JWSObject.parse(reader.readLine()); + + List certificateChain = jwsObject.getHeader().getX509CertChain().stream() + .map(c -> base64Service.encodeToString(c.decode())).collect(Collectors.toList()); + JWSAlgorithm algorithm = jwsObject.getHeader().getAlgorithm(); + + // If the x5u attribute is present in the JWT Header then + // if (jwsObject.getHeader().getX509CertURL() != null) { + // 1. The FIDO Server MUST verify that the URL specified by the x5u attribute + // has the same web-origin as the URL used to download the metadata BLOB from. + // The FIDO Server SHOULD ignore the file if the web-origin differs (in order to + // prevent loading objects from arbitrary sites). + // 2. The FIDO Server MUST download the certificate (chain) from the URL + // specified by the x5u attribute [JWS]. The certificate chain MUST be verified + // to properly chain to the metadata BLOB signing trust anchor according to + // [RFC5280]. All certificates in the chain MUST be checked for revocation + // according to [RFC5280]. + // 3. The FIDO Server SHOULD ignore the file if the chain cannot be verified or + // if one of the chain certificates is revoked. + + // the chain should be retrieved from the x5c attribute. + // else if (certificateChain.isEmpty()) { + // The FIDO Server SHOULD ignore the file if the chain cannot be verified or if + // one of the chain certificates is revoked. + // } else { + log.info("Metadata BLOB signing trust anchor is considered the BLOB signing certificate chain"); + // Metadata BLOB signing trust anchor is considered the BLOB signing certificate + // chain. + // Verify the signature of the Metadata BLOB object using the BLOB signing + // certificate chain (as determined by the steps above). The FIDO Server SHOULD + // ignore the file if the signature is invalid. It SHOULD also ignore the file + // if its number (no) is less or equal to the number of the last Metadata BLOB + // object cached locally. + // } + + try { + JWSVerifier verifier = resolveVerifier(algorithm, mdsTocRootCertsFolder, certificateChain); + if (!jwsObject.verify(verifier)) { + log.warn("Unable to verify JWS object using algorithm {} for file {}", algorithm, path); + return new Pair>(null, Collections.emptyMap()); + } + } catch (Exception e) { + log.warn("Unable to verify JWS object using algorithm {} for file {} {}", algorithm, path, e); + return new Pair>(null, Collections.emptyMap()); + } + + String jwtPayload = jwsObject.getPayload().toString(); + JsonNode toc = dataMapperService.readTree(jwtPayload); + log.debug("Legal header {}", toc.get("legalHeader")); + nextUpdate = LocalDate.parse(toc.get("nextUpdate").asText(), ISO_DATE); + + ArrayNode entries = (ArrayNode) toc.get("entries"); + int serialNo = toc.get("no").asInt(); + // The serial number of this UAF Metadata BLOB Payload. Serial numbers MUST be + // consecutive and strictly monotonic, i.e. the successor BLOB will have a no + // value exactly incremented by one. + + log.debug("Property 'no' value: {}. serialNo: {}", serialNo, entries.size()); + + Iterator iter = entries.elements(); + Map tocEntries = new HashMap<>(); + while (iter.hasNext()) { + JsonNode metadataEntry = iter.next(); + if (metadataEntry.hasNonNull("aaguid")) { + String aaguid = metadataEntry.get("aaguid").asText(); + try { + JsonNode metaDataStatement = dataMapperService + .readTree(metadataEntry.get("metadataStatement").toPrettyString()); + if (metaDataStatement != null) { + + log.info("Added TOC entry {} ", aaguid); + tocEntries.put(aaguid, metadataEntry); + } + + } catch (IOException e) { + log.error("Error parsing the metadata statement", e); + } + + } else if (metadataEntry.hasNonNull("aaid")) { + String aaid = metadataEntry.get("aaid").asText(); + log.info("TODO: handle aaid addition to tocEntries {}", aaid); + } else if (metadataEntry.hasNonNull("attestationCertificateKeyIdentifiers")) { + // FIDO U2F authenticators do not support AAID nor AAGUID, but they use + // attestation certificates dedicated to a single authenticator model. + String attestationCertificateKeyIdentifiers = metadataEntry + .get("attestationCertificateKeyIdentifiers").asText(); + log.info("TODO: handle attestationCertificateKeyIdentifiers addition to tocEntries {}", + attestationCertificateKeyIdentifiers); + } else { + log.info( + "Null - aaguid , aaid, attestationCertificateKeyIdentifiers - Added TOC entry from {} with status {}", + path, metadataEntry.get("statusReports").findValue("status")); + } + } + + String nextUpdateText = toc.get("nextUpdate").asText(); + + LocalDate nextUpdateDate = LocalDate.parse(nextUpdateText); + + this.digester = resolveDigester(algorithm); + + return new Pair>(nextUpdateDate, tocEntries); + } + } private JWSVerifier resolveVerifier(JWSAlgorithm algorithm, String mdsTocRootCertsFolder, List certificateChain) { List x509CertificateChain = certificateService.getCertificates(certificateChain); List x509TrustedCertificates = certificateService.getCertificates(mdsTocRootCertsFolder); X509Certificate verifiedCert = certificateVerifier.verifyAttestationCertificates(x509CertificateChain, x509TrustedCertificates); - + //possible set of algos are : ES256, RS256, PS256, ED256 + // no support for ED256 in JOSE library + if (JWSAlgorithm.ES256.equals(algorithm)) { + log.debug("resolveVerifier : ES256"); try { return new ECDSAVerifier((ECPublicKey) verifiedCert.getPublicKey()); } catch (JOSEException e) { throw new Fido2RuntimeException("Unable to create verifier for algorithm " + algorithm, e); } - } else { + } + else if (JWSAlgorithm.RS256.equals(algorithm) || JWSAlgorithm.PS256.equals(algorithm)) { + log.debug("resolveVerifier : RS256"); + return new RSASSAVerifier((RSAPublicKey) verifiedCert.getPublicKey()); + + } + else { throw new Fido2RuntimeException("Don't know what to do with " + algorithm); } } private MessageDigest resolveDigester(JWSAlgorithm algorithm) { - if (JWSAlgorithm.ES256.equals(algorithm)) { + // fix: algorithm RS256 added for https://github.com/GluuFederation/fido2/issues/16 + if (JWSAlgorithm.ES256.equals(algorithm) || JWSAlgorithm.RS256.equals(algorithm) ) { return DigestUtils.getSha256Digest(); - } else { + } + else { throw new Fido2RuntimeException("Don't know what to do with " + algorithm); } } @@ -235,6 +322,7 @@ private Map mergeAndResolveDuplicateEntries(List directoryStream = Files.newDirectoryStream(path)) { + Iterator iter = directoryStream.iterator(); + while (iter.hasNext()) { + Path filePath = iter.next(); + try (InputStream in = metadataUrl.openStream()) { + + Files.copy(in, filePath, StandardCopyOption.REPLACE_EXISTING); + + log.info("TOC file updated."); + return true; + } + } + } catch (IOException e) { + log.warn("Can't access or open path: {}", path, e); + } + return false; + } + +} \ No newline at end of file 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 1766360da1a..aaa05212c81 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 @@ -263,10 +263,12 @@ private Pair prepareAllowedCredentials(String documentDomain, allowedFido2Registrations.forEach((value) -> { log.debug("attestation request:" + value.getRegistrationData().getAttenstationRequest()); }); + + // 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().contains(AuthenticatorAttachment.PLATFORM.getAttachment())) + ((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" }, 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 8e3bb8d364f..ab10af982cf 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 @@ -380,4 +380,4 @@ private ArrayNode prepareExcludeCredentials(String documentDomain, String userna return excludedCredentials; } -} +} \ No newline at end of file diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/AppleAttestationProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/AppleAttestationProcessor.java index 45a40f3934f..a1d92246c63 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/AppleAttestationProcessor.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/AppleAttestationProcessor.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.JsonNode; - import io.jans.fido2.ctap.AttestationFormat; import io.jans.fido2.exception.AttestationException; import io.jans.fido2.exception.Fido2MissingAttestationCertException; @@ -86,7 +85,7 @@ public AttestationFormat getAttestationFormat() { * * Valid until 03/14/2045 @ 5:00 PM PST */ - private static final String APPLE_WEBAUTHN_ROOT_CA = "/etc/gluu/conf/fido2/apple/"; + private static final String APPLE_WEBAUTHN_ROOT_CA = "/etc/jans/conf/fido2/apple/"; // @Override public void process(JsonNode attStmt, AuthData authData, Fido2RegistrationData credential, byte[] clientDataHash, @@ -116,7 +115,8 @@ public void process(JsonNode attStmt, AuthData authData, Fido2RegistrationData c trustAnchorCertificates.addAll(certificateService.getCertificates(APPLE_WEBAUTHN_ROOT_CA)); try { log.debug("APPLE_WEBAUTHN_ROOT_CA root certificate" + trustAnchorCertificates.size()); - X509Certificate verifiedCert = certificateVerifier.verifyAttestationCertificates(certificates, trustAnchorCertificates); + X509Certificate verifiedCert = certificateVerifier.verifyAttestationCertificates(certificates, + trustAnchorCertificates); log.info("Step 1 completed "); } catch (Fido2MissingAttestationCertException ex) { 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 641efadc6c6..48d67bd5198 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 @@ -445,9 +445,18 @@ public int verifyTimeout(JsonNode params) { return timeout; } - public void verifyThatMetadataIsValid(JsonNode metadata) { - long count = Arrays.asList(metadata.hasNonNull("aaguid"), metadata.hasNonNull("assertionScheme"), metadata.hasNonNull("attestationTypes"), - metadata.hasNonNull("description")).parallelStream().filter(f -> f == false).count(); + // fix: fetching metadataStatement from the individual metadataNode (causing NPE) - also, removing metadata.hasNonNull("assertionScheme") as per MDS3 upgrade - https://medium.com/webauthnworks/webauthn-fido2-whats-new-in-mds3-migrating-from-mds2-to-mds3-a271d82cb774 + public void verifyThatMetadataIsValid(JsonNode metadata) { + + JsonNode metaDataStatement= null; + try { + metaDataStatement = dataMapperService + .readTree(metadata.get("metadataStatement").toPrettyString()); + } catch (IOException e) { + throw new Fido2RuntimeException("Unable to process metadataStatement:",e); + } + long count = Arrays.asList(metaDataStatement.hasNonNull("aaguid"), metaDataStatement.hasNonNull("attestationTypes"), + metaDataStatement.hasNonNull("description")).parallelStream().filter(f -> f == false).count(); if (count != 0) { throw new Fido2RuntimeException("Invalid parameters in metadata"); }