Skip to content

Commit

Permalink
feat(oxauth): first party applications support
Browse files Browse the repository at this point in the history
feat(oxauth): first party applications support
  • Loading branch information
yuriyz authored Oct 24, 2024
2 parents 7e4748e + e4ea049 commit f6b9997
Show file tree
Hide file tree
Showing 44 changed files with 3,403 additions and 177 deletions.
21 changes: 21 additions & 0 deletions community-edition-setup/schema/gluu_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5445,6 +5445,27 @@
],
"x_origin": "Gluu created objectclass"
},
{
"kind": "STRUCTURAL",
"may": [
"oxId",
"oxAuthUserDN",
"creationDate",
"oxAuthClientId",
"oxAttributes"
],
"must": [
"objectclass"
],
"names": [
"oxAuthzChallSess"
],
"oid": "oxObjectClass",
"sup": [
"top"
],
"x_origin": "Gluu created objectclass"
},
{
"kind": "STRUCTURAL",
"may": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from org.apache.commons.lang3 import StringUtils
from org.gluu.model import SimpleCustomProperty
from org.gluu.model.custom.script.model import CustomScript
from org.gluu.model.custom.script.type.authzchallenge import AuthorizationChallengeType
from org.gluu.oxauth.authorize.ws.rs import AuthorizationChallengeSessionService
from org.gluu.oxauth.model.common import User
from org.gluu.oxauth.model.session import AuthorizationChallengeSession
from org.gluu.oxauth.service import UserService
from org.gluu.oxauth.service.external.context import ExternalScriptContext
from org.gluu.persist import PersistenceEntryManager
from org.gluu.service.cdi.util import CdiUtil
from org.gluu.service.custom.script import CustomScriptManager
from org.slf4j import LoggerFactory

import java

class AuthorizationChallenge(AuthorizationChallengeType):
def __init__(self, currentTimeMillis):
self.currentTimeMillis = currentTimeMillis

def authorize(self, context):
# 1. As first step we get username
username = self.getParameterOrCreateError(context, "username")
if StringUtils.isBlank(username):
return False

# 2. OTP validation
otp = self.getParameterOrCreateError(context, "otp")
if StringUtils.isBlank(otp):
return False

print "All required parameters are present"

# Main authorization logic
userService = CdiUtil.bean(UserService)
entryManager = CdiUtil.bean(PersistenceEntryManager)

user = userService.getUser(username)
if user is None:
print "User is not found"
self.createError(context, "username_invalid")
return False

isUserActive = StringUtils.equals(user.getStatus(), "ACTIVE")
if not isUserActive:
print "User is not active"
self.createError(context, "username_inactive")
return False

ok = entryManager.authenticate(user.getDn(), User, otp)
if ok:
context.getExecutionContext().setUser(user)
print "User is authenticated successfully."
return True

# Error case
print "Failed to authenticate user. Please check username and OTP."
self.createError(context, "username_or_otp_invalid")
return False

def getParameterOrCreateError(self, context, parameterName):
value = context.getHttpRequest().getParameter(parameterName)

if StringUtils.isBlank(value):
value = self.getParameterFromSession(context, parameterName)

if StringUtils.isBlank(value):
self.createError(context, "%s_required" % parameterName)
return None

return value

def createError(self, context, errorCode):
sessionPart = self.prepareSessionSubJson(context)
entity = "{\"error\": \"%s\"%s}" % (errorCode, sessionPart)
context.createWebApplicationException(401, entity)

def prepareSessionSubJson(self, context):
sessionObject = context.getExecutionContext().getAuthzRequest().getAuthorizationChallengeSessionObject()
if sessionObject is not None:
self.prepareSession(context, sessionObject)
return ",\"auth_session\":\"%s\"" % sessionObject.getId()
elif context.getExecutionContext().getAuthzRequest().isUseAuthorizationChallengeSession():
sessionObject = self.prepareSession(context, None)
return ",\"auth_session\":\"%s\"" % sessionObject.getId()
return ""

def prepareSession(self, context, sessionObject):
authzSessionService = CdiUtil.bean(AuthorizationChallengeSessionService)
newSave = sessionObject is None
if newSave:
sessionObject = authzSessionService.newAuthorizationChallengeSession()

username = context.getHttpRequest().getParameter("username")
if StringUtils.isNotBlank(username):
sessionObject.getAttributes().getAttributes().put("username", username)

otp = context.getHttpRequest().getParameter("otp")
if StringUtils.isNotBlank(otp):
sessionObject.getAttributes().getAttributes().put("otp", otp)

if newSave:
authzSessionService.persist(sessionObject)
else:
authzSessionService.merge(sessionObject)

return sessionObject

def getParameterFromSession(self, context, parameterName):
sessionObject = context.getExecutionContext().getAuthzRequest().getAuthorizationChallengeSessionObject()
if sessionObject is not None:
return sessionObject.getAttributes().getAttributes().get(parameterName)
return None

def init(self, configurationAttributes):
print "Initialized Default AuthorizationChallenge Python custom script."
return True

def init(self, customScript, configurationAttributes):
print "Initialized Default AuthorizationChallenge Python custom script."
return True

def destroy(self, configurationAttributes):
print "Destroyed Default AuthorizationChallenge Python custom script."
return True

def getApiVersion(self):
return 11
11 changes: 11 additions & 0 deletions community-edition-setup/templates/scopes.ldif
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ objectClass: top
oxAttributes: {"spontaneousClientId":"","spontaneousClientScopes":[],"showInConfigurationEndpoint":true}
oxScopeType: openid

dn: inum=6D92,ou=scopes,o=jans
description: Authorization Challenge
displayName: authorization_challenge
inum: 6D92
oxAttributes: {"spontaneousClientId":"","spontaneousClientScopes":[],"showInConfigurationEndpoint":true}
defaultScope: true
oxId: authorization_challenge
oxScopeType: oauth
objectClass: top
objectClass: oxAuthCustomScope

dn: inum=7D90,ou=scopes,o=gluu
defaultScope: false
description: revoke_session scope which is required to be able call /revoke_session endpoint
Expand Down
14 changes: 14 additions & 0 deletions community-edition-setup/templates/scripts.ldif
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,20 @@ oxScript::%(resource_owner_password_credentials_resource_owner_password_credenti
oxScriptType: resource_owner_password_credentials
programmingLanguage: python

dn: inum=0300-BA99,ou=scripts,o=gluu
objectClass: top
objectClass: oxCustomScript
description: Default Authorization Challenge Script
displayName: default_challenge
oxEnabled: true
inum: 0300-BA99
oxLevel: 1
oxRevision: 1
oxModuleProperty: {"value1":"location_type","value2":"ldap","description":""}
oxScript::%(authorization_challenge_authorization_challenge)s
oxScriptType: authorization_challenge
programmingLanguage: python

dn: inum=4BBE-C6A8,ou=scripts,o=gluu
objectClass: top
objectClass: oxCustomScript
Expand Down
1 change: 1 addition & 0 deletions docs-gluu-server-prod/docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ nav:
- 'UMA Authorization Server (AS)': 'admin-guide/uma.md'
- 'Central Authentication Service (CAS)': 'admin-guide/cas.md'
- 'OAuth2.0 Device Authorization Grant': admin-guide/device-authz-grant/
- 'OAuth2.0 First-Party Applications': 'admin-guide/first-party-apps/'
- 'Session Management': 'admin-guide/session.md'
- 'Certificate Management': 'admin-guide/certificate.md'
- 'CORS Configuration': 'admin-guide/cors.md'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from org.apache.commons.lang3 import StringUtils
from org.gluu.model import SimpleCustomProperty
from org.gluu.model.custom.script.model import CustomScript
from org.gluu.model.custom.script.type.authzchallenge import AuthorizationChallengeType
from org.gluu.oxauth.authorize.ws.rs import AuthorizationChallengeSessionService
from org.gluu.oxauth.model.common import User
from org.gluu.oxauth.model.session import AuthorizationChallengeSession
from org.gluu.oxauth.service import UserService
from org.gluu.oxauth.service.external.context import ExternalScriptContext
from org.gluu.persist import PersistenceEntryManager
from org.gluu.service.cdi.util import CdiUtil
from org.gluu.service.custom.script import CustomScriptManager
from org.slf4j import LoggerFactory

import java

class AuthorizationChallenge(AuthorizationChallengeType):
def __init__(self, currentTimeMillis):
self.currentTimeMillis = currentTimeMillis

def authorize(self, context):
# 1. As first step we get username
username = self.getParameterOrCreateError(context, "username")
if StringUtils.isBlank(username):
return False

# 2. OTP validation
otp = self.getParameterOrCreateError(context, "otp")
if StringUtils.isBlank(otp):
return False

print "All required parameters are present"

# Main authorization logic
userService = CdiUtil.bean(UserService)
entryManager = CdiUtil.bean(PersistenceEntryManager)

user = userService.getUser(username)
if user is None:
print "User is not found"
self.createError(context, "username_invalid")
return False

isUserActive = StringUtils.equals(user.getStatus(), "ACTIVE")
if not isUserActive:
print "User is not active"
self.createError(context, "username_inactive")
return False

ok = entryManager.authenticate(user.getDn(), User, otp)
if ok:
context.getExecutionContext().setUser(user)
print "User is authenticated successfully."
return True

# Error case
print "Failed to authenticate user. Please check username and OTP."
self.createError(context, "username_or_otp_invalid")
return False

def getParameterOrCreateError(self, context, parameterName):
value = context.getHttpRequest().getParameter(parameterName)

if StringUtils.isBlank(value):
value = self.getParameterFromSession(context, parameterName)

if StringUtils.isBlank(value):
self.createError(context, "%s_required" % parameterName)
return None

return value

def createError(self, context, errorCode):
sessionPart = self.prepareSessionSubJson(context)
entity = "{\"error\": \"%s\"%s}" % (errorCode, sessionPart)
context.createWebApplicationException(401, entity)

def prepareSessionSubJson(self, context):
sessionObject = context.getExecutionContext().getAuthzRequest().getAuthorizationChallengeSessionObject()
if sessionObject is not None:
self.prepareSession(context, sessionObject)
return ",\"auth_session\":\"%s\"" % sessionObject.getId()
elif context.getExecutionContext().getAuthzRequest().isUseAuthorizationChallengeSession():
sessionObject = self.prepareSession(context, None)
return ",\"auth_session\":\"%s\"" % sessionObject.getId()
return ""

def prepareSession(self, context, sessionObject):
authzSessionService = CdiUtil.bean(AuthorizationChallengeSessionService)
newSave = sessionObject is None
if newSave:
sessionObject = authzSessionService.newAuthorizationChallengeSession()

username = context.getHttpRequest().getParameter("username")
if StringUtils.isNotBlank(username):
sessionObject.getAttributes().getAttributes().put("username", username)

otp = context.getHttpRequest().getParameter("otp")
if StringUtils.isNotBlank(otp):
sessionObject.getAttributes().getAttributes().put("otp", otp)

if newSave:
authzSessionService.persist(sessionObject)
else:
authzSessionService.merge(sessionObject)

return sessionObject

def getParameterFromSession(self, context, parameterName):
sessionObject = context.getExecutionContext().getAuthzRequest().getAuthorizationChallengeSessionObject()
if sessionObject is not None:
return sessionObject.getAttributes().getAttributes().get(parameterName)
return None

def init(self, configurationAttributes):
print "Initialized Default AuthorizationChallenge Python custom script."
return True

def init(self, customScript, configurationAttributes):
print "Initialized Default AuthorizationChallenge Python custom script."
return True

def destroy(self, configurationAttributes):
print "Destroyed Default AuthorizationChallenge Python custom script."
return True

def getApiVersion(self):
return 11
37 changes: 37 additions & 0 deletions docs-gluu-server-prod/docs/source/admin-guide/custom-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,43 @@ This script can be used in an oxTrust application only.

- [Sample ID Generation Script](./sample-id-generation-script.py)


## Authorization Challenge Custom Script

The Authorization server implements [OAuth 2.0 for First-Party Applications](https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-native-apps-02.html).
This script is used to control/customize Authorization Challenge Endpoint.

In request to Authorization Challenge Endpoint to is expected to have `acr_values` request parameter which specifies name of the custom script.
If parameter is absent or AS can't find script with this name then it falls back to script with name `default_challenge`.
This script is provided during installation and performs basic `username`/`password` authentication.

```
POST /oxauth/restv1/authorize-challenge HTTP/1.1
Host: yuriyz-fond-skink.gluu.info
client_id=999e13b8-f4a2-4fed-ad3c-6c88bd2c92ea&scope=openid+profile+address+email+phone+user_name&state=b4a41b29-51c8-4354-9c8c-fda38b4dbd43&nonce=3a56f8d0-f78e-4b15-857c-3e792801be68&acr_values=&request_session_id=false&password=secret&username=admin
```
There is **authorizationChallengeDefaultAcr** AS configuration property which allows to change fallback script name from `default_challenge` to some other value (value must be valid script name present on AS).

The Authorization Challenage script implements the [AuthorizationChallenageType](https://github.com/GluuFederation/oxauth/blob/4.5/oxCore/script/src/main/java/org/gluu/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java) interface. This extends methods from the base script type in addition to adding new methods:

**Inherited methods**
| Method header | Method description |
|:-----|:------|
| `def init(self, customScript, configurationAttributes)` | This method is only called once during the script initialization. It can be used for global script initialization, initiate objects etc |
| `def destroy(self, configurationAttributes)` | This method is called once to destroy events. It can be used to free resource and objects created in the `init()` method |
| `def getApiVersion(self, configurationAttributes, customScript)` | The getApiVersion method allows API changes in order to do transparent migration from an old script to a new API. Only include the customScript variable if the value for getApiVersion is greater than 10 |

**New methods**
| Method header | Method description |
|:-----|:------|
|`def authorize(self, context)`| Called when the request is received. |

`authorize` method returns true/false which indicates to server whether to issue `authorization_code` in response or not.
If parameters is not present then error has to be created and `false` returned.
If all is good script has to return `true` and it's strongly recommended to set user `context.getExecutionContext().setUser(user);` so AS can keep tracking what exactly user is authenticated.

Full sample script can be found [here](./authorization_challenge.py)

## Cache Refresh

In order to integrate your Gluu instance with backend LDAP servers handling authentication in your existing network environment, oxTrust provides a mechanism called [Cache Refresh](../user-management/ldap-sync.md#ldap-synchronization) to copy user data to the Gluu Server's local LDAP server. During this process it is possible to specify key attribute(s) and specify attribute name transformations. There are also cases when it can be used to overwrite attribute values or to add new attributes based on other attribute values.
Expand Down
Loading

0 comments on commit f6b9997

Please sign in to comment.