Skip to content

Commit

Permalink
feat(jans-auth-server): OAuth 2.0 for First-Party Native Applications (
Browse files Browse the repository at this point in the history
…#5654)

* feat(jans-auth-server): added authorization challenge endpoint config and discovery #5563

* feat(jans-auth-server): renamed authorization_challenge_request_endpoint -> authorization_challenge_endpoint

#5563

* feat(jans-auth-server): added device session with attributes #5563

* feat(jans-auth-server): added authorization challenge custom script #5563

* feat(jans-auth-server): implemented authorization challenge service #5563

* feat(jans-auth-server): added authorization challenge validator with test #5563

* feat(jans-auth-server): added device session service #5563

* feat(jans-auth-server): buildfix #5563

* feat(jans-auth-server): added session support to authorization challenge #5563

* feat(jans-auth-server): authorization challenge endpoint #5563

* feat(jans-auth-server): minor improvements in authorization endpoint #5563

* feat(jans-auth-server): improved external script context (need it for authorization challenge) #5563

* feat(jans-auth-server): added external service impl for authz challenge #5781

* fix(jans-auth-server): added validation test to testng

* feat(jans-auth-server): added sample custom script for authz challenge #5781

* feat(jans-linux-setup): added default authz challenge custom script to setup #5781

* fix(jans-linux-setup): fixed path to authz challenge custom script #5781

* chore(jans-auth-server): removed redundant code #5563

* fix(jans-auth-server): fixed authz challenge endpoint initialization #5563

* fix(jans-auth-server): corrected challenge endpoint #5563

* fix(jans-auth-server): npe fix in challenge endpoint #5563

* test(jans-auth-server): added integration test for authorization challenge #5563

* doc(jans-auth-server): documented Authorization Challenge implementation #5563

Signed-off-by: Mustafa Baser <mbaser@mail.com>
  • Loading branch information
yuriyz authored and devrimyatar committed Dec 30, 2023
1 parent 69f82af commit 6849fe5
Show file tree
Hide file tree
Showing 51 changed files with 2,433 additions and 49 deletions.
636 changes: 636 additions & 0 deletions docs/admin/auth-server/endpoints/authorization-challenge.md

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions docs/admin/auth-server/endpoints/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ https://janssen.server.host/jans-auth/restv1/authorize
More information about request and response of the authorization endpoint can be found in the OpenAPI specification
of [jans-auth-server module](https://gluu.org/swagger-ui/?url=https://raw.githubusercontent.com/JanssenProject/jans/vreplace-janssen-version/jans-auth-server/docs/swagger.yaml#/Authorization).

## Disabling The Endpoint Using Feature Flag

TODO: It seems this endpoint can't be disabled using featureflags. Confirm this.


## Configuration Properties

Expand Down
1 change: 1 addition & 0 deletions docs/admin/auth-server/oauth-features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The [Janssen Authentication Server](https://github.com/JanssenProject/jans/tree/
- OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens [(spec)](https://datatracker.ietf.org/doc/html/rfc8705)
- Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants [(spec)](https://www.rfc-editor.org/rfc/rfc7521.html)
- JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) [(spec)](https://openid.net/specs/oauth-v2-jarm.html)
- OAuth 2.0 for First-Party Native Applications [(spec)](https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-native-apps-00.html)


## Protocol Overview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ It returns all the information of the Jans Authorization server.
"issuer": "https://example.jans.io",
"baseEndpoint": "https://example.jans.io/jans-auth/restv1",
"authorizationEndpoint": "https://example.jans.io/jans-auth/restv1/authorize",
"authorizationChallengeEndpoint":"https://example.jans.io/jans-auth/restv1/authorization_challenge",
"tokenEndpoint": "https://example.jans.io/jans-auth/restv1/token",
"tokenRevocationEndpoint": "https://example.jans.io/jans-auth/restv1/revoke",
"userInfoEndpoint": "https://example.jans.io/jans-auth/restv1/userinfo",
Expand Down
1 change: 1 addition & 0 deletions docs/admin/developer/interception-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ calling external APIs
1. SCIM
1. [Introspection](./scripts/introspection.md) : Introspection scripts allows to modify response of Introspection Endpoint spec and present additional meta information surrounding the token.
1. [Post Authentication](./scripts/post-authentication.md)
1. [Authorization Challenge](./scripts/authorization-challenge.md)
1. [Select Account](./scripts/select-account.md)
1. Resource Owner Password Credentials
1. UMA 2 RPT Authorization Policies
Expand Down
246 changes: 246 additions & 0 deletions docs/admin/developer/scripts/authorization-challenge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
---
tags:
- administration
- developer
- scripts
---

# Authorization Challenge Custom Script

## Overview

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

## Behavior

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 /jans-auth/restv1/authorization_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).


## Interface
The Authorization Challenage script implements the [AuthorizationChallenageType](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/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.


### Objects
| Object name | Object description |
|:-----|:------|
|`customScript`| The custom script object. [Reference](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/model/CustomScript.java) |
|`context`| [Reference](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java) |


## Common Use Case: Authorize user by username/password

### Script Type: Java

```java
import io.jans.as.common.model.common.User;
import io.jans.as.common.model.session.DeviceSession;
import io.jans.as.server.authorize.ws.rs.DeviceSessionService;
import io.jans.as.server.service.UserService;
import io.jans.as.server.service.external.context.ExternalScriptContext;
import io.jans.model.SimpleCustomProperty;
import io.jans.model.custom.script.model.CustomScript;
import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType;
import io.jans.orm.PersistenceEntryManager;
import io.jans.service.cdi.util.CdiUtil;
import io.jans.service.custom.script.CustomScriptManager;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.UUID;

/**
* @author Yuriy Z
*/
public class AuthorizationChallenge implements AuthorizationChallengeType {

public static final String USERNAME_PARAMETER = "username";
public static final String PASSWORD_PARAMETER = "password";

private static final Logger log = LoggerFactory.getLogger(AuthorizationChallenge.class);
private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class);

/**
* Return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned.
* <p>
* Implementation of this method should consist of 3 main parts:
* 1. validate all parameters are present and if not -> set error and return false
* 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true
* 3. if not ok -> set error which explains what is wrong and return false
*
* @param scriptContext ExternalScriptContext, see https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java
* @return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned.
*/
@Override
public boolean authorize(Object scriptContext) {
ExternalScriptContext context = (ExternalScriptContext) scriptContext;

// 1. validate all required parameters are present
final String username = getParameterOrCreateError(context, USERNAME_PARAMETER);
if (StringUtils.isBlank(username)) {
return false;
}

final String password = getParameterOrCreateError(context, PASSWORD_PARAMETER);
if (StringUtils.isBlank(password)) {
return false;
}

scriptLogger.trace("All required parameters are present");

// 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true
UserService userService = CdiUtil.bean(UserService.class);
PersistenceEntryManager entryManager = CdiUtil.bean(PersistenceEntryManager.class);

final User user = userService.getUser(username);
if (user == null) {
scriptLogger.trace("User is not found by username {}", username);
createError(context, "username_invalid");
return false;
}

final boolean ok = entryManager.authenticate(user.getDn(), User.class, password);
if (ok) {
context.getExecutionContext().setUser(user); // <- IMPORTANT : without user set, user relation will not be associated with token
scriptLogger.trace("User {} is authenticated successfully.", username);
return true;
}

// 3. not ok -> set error which explains what is wrong and return false
scriptLogger.trace("Failed to authenticate user {}. Please check username and password.", username);
createError(context, "username_or_password_invalid");
return false;
}

private String getParameterOrCreateError(ExternalScriptContext context, String parameterName) {
String value = context.getHttpRequest().getParameter(parameterName);

if (StringUtils.isBlank(value)) {
scriptLogger.trace("No '{}' parameter in request", parameterName);
value = getParameterFromDeviceSession(context, parameterName);
}

if (StringUtils.isBlank(value)) {
scriptLogger.trace("{} is not provided", parameterName);
createError(context, String.format("%s_required", parameterName));
return null;
}

return value;
}

private void createError(ExternalScriptContext context, String errorCode) {
String deviceSessionPart = prepareDeviceSessionSubJson(context);

final String entity = String.format("{\"error\": \"%s\"%s}", errorCode, deviceSessionPart);
context.createWebApplicationException(401, entity);
}

private String prepareDeviceSessionSubJson(ExternalScriptContext context) {
DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject();
if (deviceSessionObject != null) {
prepareDeviceSession(context, deviceSessionObject);
return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId());
} else if (context.getAuthzRequest().isUseDeviceSession()) {
deviceSessionObject = prepareDeviceSession(context, null);
return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId());
}
return "";
}

private DeviceSession prepareDeviceSession(ExternalScriptContext context, DeviceSession deviceSessionObject) {
DeviceSessionService deviceSessionService = CdiUtil.bean(DeviceSessionService.class);
boolean newSave = deviceSessionObject == null;
if (newSave) {
final String id = UUID.randomUUID().toString();
deviceSessionObject = new DeviceSession();
deviceSessionObject.setId(id);
deviceSessionObject.setDn(deviceSessionService.buildDn(id));
}

String username = context.getHttpRequest().getParameter(USERNAME_PARAMETER);
if (StringUtils.isNotBlank(username)) {
deviceSessionObject.getAttributes().getAttributes().put(USERNAME_PARAMETER, username);
}

String password = context.getHttpRequest().getParameter(PASSWORD_PARAMETER);
if (StringUtils.isNotBlank(password)) {
deviceSessionObject.getAttributes().getAttributes().put(PASSWORD_PARAMETER, password);
}

if (newSave) {
deviceSessionService.persist(deviceSessionObject);
} else {
deviceSessionService.merge(deviceSessionObject);
}

return deviceSessionObject;
}

private String getParameterFromDeviceSession(ExternalScriptContext context, String parameterName) {
final DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject();
if (deviceSessionObject != null) {
return deviceSessionObject.getAttributes().getAttributes().get(parameterName);
}
return null;
}

@Override
public boolean init(Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script.");
return true;
}

@Override
public boolean init(CustomScript customScript, Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script.");
return true;
}

@Override
public boolean destroy(Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Destroyed Default AuthorizationChallenge Java custom script.");
return true;
}

@Override
public int getApiVersion() {
return 11;
}
}

```

### Sample Scripts
- [AuthorizationChallenge](../../../script-catalog/authorization_challenge/AuthorizationChallenge.java)
Loading

0 comments on commit 6849fe5

Please # to comment.