Skip to content

Commit

Permalink
fix: #817 - script for DUO should have the universal prompt, other AP…
Browse files Browse the repository at this point in the history
…Is are deprecated + documentation minor fixes
  • Loading branch information
maduvena committed Sep 12, 2022
1 parent aed6ee3 commit 5ccc8c2
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 170 deletions.
9 changes: 9 additions & 0 deletions docs/admin/developer/customization/customize-web-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,12 @@ Resource bundle names to support other languages should be placed under the same
### Custom image files:
1. All images should be placed under `/opt/gluu/jetty/jans-auth/custom/static/img`
2. Reference it in .xhtml file using the URL `https://your.jans.server/oxauth/ext/resources/img/fileName.png` or `/oxauth/ext/resources/img/fileName.jpg`

### Page layout, header, footer (xhtml Template) customization

Templates refers to the common interface layout and style. For example, a same banner, logo in common header and copyright information in footer.

1. `mkdir -p /opt/jans/jetty/jans-auth/custom/pages/WEB-INF/incl/layout/`
2. Place a modified `template.xhtml` in the above location which will override the [default template file](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/template.xhtml) from the war


12 changes: 5 additions & 7 deletions docs/admin/developer/scripts/person-authentication.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@


# Adaptive Authentication scripts (Person Authentication scripts)
# Person Authentication scripts
The Jans-Auth Server leverages interception scripts of [PersonAuthenticationType](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/type/auth/PersonAuthenticationType.java) which when implemented can facilitate complex multi-step, multi-factor authentication workflows. The authentication flow in the Jans Server is driven by the openID spec. The authorization request to the OP (Jans server) contains an optional query parameter called `acr_values` which is used by the OP to pick an interception script which will be run when `/authorize` endpoint (Authentication flow) is invoked. The name of each script corresponds with its `acr` value in the Jans-Auth Server.

Typically, a `PersonAuthenticationType` script can be used to:
Expand Down Expand Up @@ -58,24 +56,24 @@ All pages are **xhtml** files. The Jans-auth server comes with a default set of
This [article](https://github.com/maduvena/jans-docs/wiki/Writing-UI-pages) covers all the details you need to write your own web page.

## Building business logic in Custom script:
Jans-auth server uses Weld 3.0 (JSR-365 aka CDI 2.0) for managed beans. The most important aspects of business logic are implemented through a set of beans. Details and examples of this can be found in this [article](https://jans.io/docs/admin/developer/managed-beans)
Jans-auth server uses Weld 3.0 (JSR-365 aka CDI 2.0) for managed beans. The most important aspects of business logic are implemented through a set of beans. Details and examples of this can be found in this [article](../developer/managed-beans)

## Adding libraries for use in the custom script
Java or Python libraries to be imported and used very easily. Remember incase you opt for a python library, it should be written in "pure python" only.
More details of this mentioned [here](https://jans.io/docs/admin/developer/interception-scripts/#using-python-libraries-in-a-script)
More details of this mentioned [here](../interception-scripts/#using-python-libraries-in-a-script)

## Uses of Person Authentication script

### A. Implementing 2FA authentication mechanisms
1. [FIDO2](https://jans.io/docs/admin/developer/scripts/person-authentication-fido2) : Authentications using platform authenticators embedded into a person's device or physical USB, NFC or Bluetooth security keys that are inserted into a USB slot of a computer
1. [FIDO2](/../../../script-catalog/person_authentication/fido2-external-authenticator/README) : Authentications using platform authenticators embedded into a person's device or physical USB, NFC or Bluetooth security keys that are inserted into a USB slot of a computer
2. SMS OTP :
3. Email OTP

### B. Implementing Multistep authentication
1. [Redirect to previous step](https://github.com/JanssenProject/jans/blob/main/jans-linux-setup/jans_setup/static/extension/person_authentication/other/basic.reset_to_step/BasicResetToStepExternalAuthenticator.py): The script here an example of how the number of steps can be varied depending on the context or business requirement.

### C. Implementing Social logins
You can use a `PersonAuthenticationType` script to allow users to sign using credentials from popular **Social Identity providers** or **Inbound Identity Providers** like Facebook, Google and Apple. After users authenticate, thier Social Identity Provider credentials are provisioned into the Jans-auth server. More on this topic in this [article](https://jans.io/docs/admin/recipes/social-login/)
You can use a `PersonAuthenticationType` script to allow users to sign using credentials from popular **Social Identity providers** or **Inbound Identity Providers** like Facebook, Google and Apple. After users authenticate, thier Social Identity Provider credentials are provisioned into the Jans-auth server. More on this topic in this [article](../recipes/social-login/)

### D. Proactively perform fraud detection
1. Impossible travel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,76 +1,55 @@
# Janssen Project software is available under the Apache 2.0 License (2004). See http://www.apache.org/licenses/ for full text.
# Copyright (c) 2020, Janssen Project
#
# Author: Yuriy Movchan
#

from io.jans.service.cdi.util import CdiUtil
from io.jans.as.server.security import Identity
from io.jans.model.custom.script.type.auth import PersonAuthenticationType
from io.jans.as.server.service import AuthenticationService
from io.jans.as.server.service import UserService
from io.jans.service import MailService
from io.jans.util import ArrayHelper
from io.jans.as.server.service import AuthenticationService, SessionIdService
from io.jans.as.server.service.common import UserService
from io.jans.util import StringHelper
from io.jans.util import ArrayHelper
from java.util import Arrays
from io.jans.as.server.service.net import HttpService
import os
import java
import sys
from com.duosecurity import Client
from com.duosecurity.exception import DuoException
from com.duosecurity.model import Token
from io.jans.jsf2.service import FacesService
from jakarta.faces.context import FacesContext
from io.jans.jsf2.message import FacesMessages
from io.jans.as.server.util import ServerUtil

import duo_web
import json

class PersonAuthentication(PersonAuthenticationType):
def __init__(self, currentTimeMillis):
self.currentTimeMillis = currentTimeMillis

def init(self, customScript, configurationAttributes):
print "Duo. Initialization"

duo_creds_file = configurationAttributes.get("duo_creds_file").getValue2()
# Load credentials from file
f = open(duo_creds_file, 'r')
try:
creds = json.loads(f.read())
except:
print "Duo. Initialization. Failed to load creds from file:", duo_creds_file
return False
finally:
f.close()

self.ikey = str(creds["ikey"])
self.skey = str(creds["skey"])
self.akey = str(creds["akey"])

self.use_duo_group = False
if (configurationAttributes.containsKey("duo_group")):
self.duo_group = configurationAttributes.get("duo_group").getValue2()
self.use_duo_group = True
print "Duo. Initialization. Using Duo only if user belong to group:", self.duo_group

self.use_audit_group = False
if (configurationAttributes.containsKey("audit_group")):
self.audit_group = configurationAttributes.get("audit_group").getValue2()

if (not configurationAttributes.containsKey("audit_group_email")):
print "Duo. Initialization. Property audit_group_email is not specified"
return False

self.audit_email = configurationAttributes.get("audit_group_email").getValue2()
self.use_audit_group = True

print "Duo. Initialization. Using audito group:", self.audit_group
print "Duo-Universal. Initialization"

if (not configurationAttributes.containsKey("client_id")):
print "Duo Universal. Initialization. Property client_id is not specified"
return False
else:
self.client_id = configurationAttributes.get("client_id").getValue2()

if (not configurationAttributes.containsKey("client_secret")):
print "Duo Universal. Initialization. Property client_secret is not specified"
return False
else:
self.client_secret = configurationAttributes.get("client_secret").getValue2()

if (not configurationAttributes.containsKey("api_hostname")):
print "Duo Universal. Initialization. Property api_hostname is not specified"
return False
else:
self.api_hostname = configurationAttributes.get("api_hostname").getValue2()

if (self.use_duo_group or self.use_audit_group):
if (not configurationAttributes.containsKey("audit_attribute")):
print "Duo. Initialization. Property audit_attribute is not specified"
return False
else:
self.audit_attribute = configurationAttributes.get("audit_attribute").getValue2()

print "Duo. Initialized successfully"
print "Duo-Universal. Initialized successfully"
return True

def destroy(self, configurationAttributes):
print "Duo. Destroy"
print "Duo. Destroyed successfully"
print "Duo-Universal. Destroy"
print "Duo-Universal. Destroyed successfully"
return True

def getApiVersion(self):
Expand All @@ -86,121 +65,101 @@ def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes)
return None

def authenticate(self, configurationAttributes, requestParameters, step):
duo_host = configurationAttributes.get("duo_host").getValue2()

authenticationService = CdiUtil.bean(AuthenticationService)

print "Duo-Universal. Authenticate for step %s" % step

identity = CdiUtil.bean(Identity)

if (step == 1):
print "Duo. Authenticate for step 1"
authenticationService = CdiUtil.bean(AuthenticationService)

# Check if user authenticated already in another custom script
user = authenticationService.getAuthenticatedUser()

if user == None:
print "user is none"
credentials = identity.getCredentials()

user_name = credentials.getUsername()
user_password = credentials.getPassword()

logged_in = False
if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)):
userService = CdiUtil.bean(UserService)
logged_in = authenticationService.authenticate(user_name, user_password)

if (not logged_in):
print "return false"
return False

user = authenticationService.getAuthenticatedUser()

if (self.use_duo_group):
print "Duo. Authenticate for step 1. Checking if user belong to Duo group"
is_member_duo_group = self.isUserMemberOfGroup(user, self.audit_attribute, self.duo_group)
if (is_member_duo_group):
print "Duo. Authenticate for step 1. User '" + user.getUserId() + "' member of Duo group"
duo_count_login_steps = 2
else:
self.processAuditGroup(user)
duo_count_login_steps = 1

identity.setWorkingParameter("duo_count_login_steps", duo_count_login_steps)

identity.setWorkingParameter('username',user_name)
return True

elif (step == 2):
print "Duo. Authenticate for step 2"
user = authenticationService.getAuthenticatedUser()
if user == None:
print "Duo. Authenticate for step 2. Failed to determine user name"
return False

user_name = user.getUserId()

sig_response_array = requestParameters.get("sig_response")
if ArrayHelper.isEmpty(sig_response_array):
print "Duo. Authenticate for step 2. sig_response is empty"
return False

duo_sig_response = sig_response_array[0]

print "Duo. Authenticate for step 2. duo_sig_response: " + duo_sig_response

authenticated_username = duo_web.verify_response(self.ikey, self.skey, self.akey, duo_sig_response)

print "Duo. Authenticate for step 2. authenticated_username: " + authenticated_username + ", expected user_name: " + user_name

if (not StringHelper.equals(user_name, authenticated_username)):
return False

self.processAuditGroup(user)

return True

identity = CdiUtil.bean(Identity)

state = ServerUtil.getFirstValue(requestParameters, "state")
# Get state to verify consistency and originality
if identity.getWorkingParameter('state_duo') == state :

# Get authorization token to trade for 2FA
duoCode = ServerUtil.getFirstValue(requestParameters, "duo_code")
try:
token = self.duo_client.exchangeAuthorizationCodeFor2FAResult(duoCode, identity.getWorkingParameter('username'))
print "token status %s " % token.getAuth_result().getStatus()
except:
# Handle authentication failure.
print "authentication failure", sys.exc_info()[1]
return False

# User successfully passed Duo authentication.

if "allow" == token.getAuth_result().getStatus():
return True

return False

else:
print "Neither step 1 or 2"
return False

def prepareForStep(self, configurationAttributes, requestParameters, step):
identity = CdiUtil.bean(Identity)
authenticationService = CdiUtil.bean(AuthenticationService)

duo_host = configurationAttributes.get("duo_host").getValue2()

print "Duo-Universal. Prepare for step %s" % step

if (step == 1):
print "Duo. Prepare for step 1"

return True
elif (step == 2):
print "Duo. Prepare for step 2"

user = authenticationService.getAuthenticatedUser()
if (user == None):
print "Duo. Prepare for step 2. Failed to determine user name"
return False
user_name = user.getUserId()

duo_sig_request = duo_web.sign_request(self.ikey, self.skey, self.akey, user_name)
print "Duo. Prepare for step 2. duo_sig_request: " + duo_sig_request

identity.setWorkingParameter("duo_host", duo_host)
identity.setWorkingParameter("duo_sig_request", duo_sig_request)
identity = CdiUtil.bean(Identity)
user_name = identity.getWorkingParameter('username')
facesContext = CdiUtil.bean(FacesContext)
request = facesContext.getExternalContext().getRequest()
httpService = CdiUtil.bean(HttpService)
url = httpService.constructServerUrl(request) + "/postlogin.htm"

try:
self.duo_client = Client(self.client_id,self.client_secret,self.api_hostname,url)
self.duo_client.healthCheck()
except:
print "Duo-Universal. Duo config error. Verify the values in Duo-Universal.conf are correct ", sys.exc_info()[1]

state = self.duo_client.generateState()
identity.setWorkingParameter("state_duo",state)
prompt_uri = self.duo_client.createAuthUrl(user_name, state)

facesService = CdiUtil.bean(FacesService)
facesService.redirectToExternalURL(prompt_uri )

return True
return True

else:
return False

def getExtraParametersForStep(self, configurationAttributes, step):
if step == 2:
return Arrays.asList("duo_count_login_steps", "cas2_user_uid")

return None
return Arrays.asList("state_duo", "username")

def getCountAuthenticationSteps(self, configurationAttributes):
identity = CdiUtil.bean(Identity)
if (identity.isSetWorkingParameter("duo_count_login_steps")):
return int(identity.getWorkingParameter("duo_count_login_steps"))

return 2

def getPageForStep(self, configurationAttributes, step):
if (step == 2):
return "/auth/duo/duologin.xhtml"
print "Duo-Universal. getPageForStep - %s " % step
return ""

def getNextStep(self, configurationAttributes, requestParameters, step):
Expand All @@ -212,28 +171,4 @@ def getLogoutExternalUrl(self, configurationAttributes, requestParameters):

def logout(self, configurationAttributes, requestParameters):
return True

def isUserMemberOfGroup(self, user, attribute, group):
is_member = False
member_of_list = user.getAttributeValues(attribute)
if (member_of_list != None):
for member_of in member_of_list:
if StringHelper.equalsIgnoreCase(group, member_of) or member_of.endswith(group):
is_member = True
break

return is_member

def processAuditGroup(self, user):
if (self.use_audit_group):
is_member = self.isUserMemberOfGroup(user, self.audit_attribute, self.audit_group)
if (is_member):
print "Duo. Authenticate for processAuditGroup. User '" + user.getUserId() + "' member of audit group"
print "Duo. Authenticate for processAuditGroup. Sending e-mail about user '" + user.getUserId() + "' login to", self.audit_email

# Send e-mail to administrator
user_id = user.getUserId()
mailService = CdiUtil.bean(MailService)
subject = "User log in: " + user_id
body = "User log in: " + user_id
mailService.sendMail(self.audit_email, subject, body)

Loading

0 comments on commit 5ccc8c2

Please sign in to comment.