-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(jans-auth-server): OAuth 2.0 for First-Party Native Applications (…
…#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
1 parent
69f82af
commit 6849fe5
Showing
51 changed files
with
2,433 additions
and
49 deletions.
There are no files selected for viewing
636 changes: 636 additions & 0 deletions
636
docs/admin/auth-server/endpoints/authorization-challenge.md
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
246 changes: 246 additions & 0 deletions
246
docs/admin/developer/scripts/authorization-challenge.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.