Introduction to Rich Authorization Requests (RAR)
OAuth 2.0 Rich Authorization Requests (RAR) is an extension (RFC 9396) that allows clients to request fine-grained, structured authorization beyond simple scopes. It enables detailed authorization requests with specific parameters, conditions, and constraints for each authorization type.
System Architecture Overview
Rich Authorization Requests Architecture ├── Authorization Request Structure │ ├── authorization_details (JSON array) │ ├── Multiple authorization types │ ├── Type-specific parameters │ └── Conditions and constraints ├── Supported Authorization Types │ ├── Payment initiation (payment) │ ├── Account information (account_information) │ ├── Document access (document_access) │ ├── Location data (location) │ └── Custom types ├── Authorization Server Processing │ ├── Request validation │ ├── Type-specific handling │ ├── Consent gathering │ └── Token issuance with embedded details └── Protected Resource Validation ├── Token introspection ├── Authorization details verification ├── Scope enforcement └── Conditional access
Core Implementation
1. Maven Dependencies
<properties>
<spring.boot.version>3.2.0</spring.boot.version>
<nimbus.version>9.37.3</nimbus.version>
<json.path.version>2.8.0</json.path.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Nimbus JOSE + JWT -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus.version}</version>
</dependency>
<!-- JSON Path for querying -->
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${json.path.version}</version>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
2. RAR Core Data Models
package com.oauth.rar.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* Rich Authorization Request - root object
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class RichAuthorizationRequest {
@NotBlank
private String clientId;
@NotBlank
private String responseType;
private String redirectUri;
private String scope;
private String state;
@NotNull
private List<AuthorizationDetail> authorizationDetails;
private Map<String, Object> additionalParameters;
}
/**
* Base authorization detail class (polymorphic)
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = PaymentAuthorizationDetail.class, name = "payment"),
@JsonSubTypes.Type(value = AccountInformationAuthorizationDetail.class, name = "account_information"),
@JsonSubTypes.Type(value = DocumentAccessAuthorizationDetail.class, name = "document_access"),
@JsonSubTypes.Type(value = LocationAuthorizationDetail.class, name = "location"),
@JsonSubTypes.Type(value = CustomAuthorizationDetail.class, name = "custom")
})
public abstract class AuthorizationDetail {
@NotBlank
private String type;
private List<String> locations;
private Map<String, Object> actions;
private Map<String, Object> extensions;
}
/**
* Payment authorization detail (payment initiation)
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PaymentAuthorizationDetail extends AuthorizationDetail {
private PaymentType paymentType;
@NotBlank
private String debtorAccount;
@NotBlank
private String creditorAccount;
@NotBlank
private String creditorName;
private String creditorAgent;
@NotNull
private BigDecimal amount;
@NotBlank
private String currency;
private String remittanceInformation;
private LocalDateTime requestedExecutionDate;
private Boolean recurringPayment;
private RecurringDetails recurringDetails;
public enum PaymentType {
SINGLE,
PERIODIC,
BULK
}
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public static class RecurringDetails {
private String frequency;
private LocalDateTime startDate;
private LocalDateTime endDate;
private BigDecimal maximumAmount;
}
}
/**
* Account information authorization detail
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AccountInformationAuthorizationDetail extends AuthorizationDetail {
private List<String> accountIds;
private List<AccountPermission> permissions;
private Boolean includeBalance;
private Boolean includeTransactions;
private LocalDateTime transactionFrom;
private LocalDateTime transactionTo;
private Integer transactionLimit;
private Boolean recurringAccess;
public enum AccountPermission {
VIEW_BASIC,
VIEW_BALANCE,
VIEW_TRANSACTIONS,
VIEW_DETAILS
}
}
/**
* Document access authorization detail
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class DocumentAccessAuthorizationDetail extends AuthorizationDetail {
private List<String> documentIds;
private List<DocumentType> documentTypes;
private DocumentPermission permission;
private Boolean allowDownload;
private Boolean allowPrint;
private LocalDateTime accessUntil;
public enum DocumentType {
STATEMENT,
INVOICE,
CONTRACT,
REPORT,
IMAGE
}
public enum DocumentPermission {
READ,
WRITE,
DELETE,
SHARE
}
}
/**
* Location authorization detail
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class LocationAuthorizationDetail extends AuthorizationDetail {
private LocationPrecision precision;
private List<LocationCoordinates> allowedAreas;
private List<String> excludedAreas;
private Integer maxUpdates;
private LocalDateTime validUntil;
private Boolean backgroundTracking;
public enum LocationPrecision {
EXACT,
APPROXIMATE,
CITY_LEVEL,
COUNTRY_LEVEL
}
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public static class LocationCoordinates {
private double latitude;
private double longitude;
private double radius; // in meters
}
}
/**
* Custom authorization detail for extensibility
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class CustomAuthorizationDetail extends AuthorizationDetail {
private Map<String, Object> customParameters;
}
/**
* Authorization response with RAR details
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class AuthorizationResponse {
private String code;
private String state;
private List<AuthorizedDetail> authorizedDetails;
private Map<String, Object> additionalData;
}
/**
* Authorized detail (included in token response)
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class AuthorizedDetail {
private String type;
private Map<String, Object> authorizedData;
private List<String> grantedActions;
private Map<String, Object> constraints;
}
/**
* RAR Token (extended access token)
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class RARAccessToken {
private String tokenValue;
private String tokenType;
private Long expiresIn;
private String scope;
private List<AuthorizedDetail> authorizationDetails;
private Map<String, Object> additionalClaims;
}
3. RAR Request Validator
package com.oauth.rar.validator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import com.oauth.rar.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Pattern;
@Slf4j
@Component
public class RARRequestValidator implements Validator {
private final ObjectMapper objectMapper = new ObjectMapper();
// Validation patterns
private static final Pattern IBAN_PATTERN =
Pattern.compile("[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}");
private static final Pattern BIC_PATTERN =
Pattern.compile("[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?");
// Validation errors
public static class ValidationError {
public static final String MISSING_TYPE = "authorization_details.type.missing";
public static final String INVALID_TYPE = "authorization_details.type.invalid";
public static final String MISSING_DEBTOR_ACCOUNT = "payment.debtor_account.missing";
public static final String INVALID_DEBTOR_ACCOUNT = "payment.debtor_account.invalid";
public static final String MISSING_CREDITOR_ACCOUNT = "payment.creditor_account.missing";
public static final String INVALID_CREDITOR_ACCOUNT = "payment.creditor_account.invalid";
public static final String MISSING_AMOUNT = "payment.amount.missing";
public static final String INVALID_AMOUNT = "payment.amount.invalid";
public static final String MISSING_ACCOUNT_IDS = "account_information.account_ids.missing";
public static final String INVALID_PERMISSION = "account_information.permission.invalid";
public static final String MISSING_DOCUMENT_IDS = "document_access.document_ids.missing";
public static final String INVALID_LOCATION = "location.coordinates.invalid";
}
@Override
public boolean supports(Class<?> clazz) {
return RichAuthorizationRequest.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
RichAuthorizationRequest request = (RichAuthorizationRequest) target;
// Validate basic OAuth parameters
validateBasicParams(request, errors);
// Validate authorization details
if (request.getAuthorizationDetails() == null || request.getAuthorizationDetails().isEmpty()) {
errors.reject("authorization_details.empty", "At least one authorization detail is required");
return;
}
// Validate each authorization detail
for (int i = 0; i < request.getAuthorizationDetails().size(); i++) {
AuthorizationDetail detail = request.getAuthorizationDetails().get(i);
validateAuthorizationDetail(detail, i, errors);
}
}
private void validateBasicParams(RichAuthorizationRequest request, Errors errors) {
if (request.getClientId() == null || request.getClientId().trim().isEmpty()) {
errors.rejectValue("clientId", "client_id.missing", "Client ID is required");
}
if (request.getResponseType() == null || request.getResponseType().trim().isEmpty()) {
errors.rejectValue("responseType", "response_type.missing", "Response type is required");
} else if (!"code".equals(request.getResponseType()) && !"token".equals(request.getResponseType())) {
errors.rejectValue("responseType", "response_type.invalid", "Invalid response type");
}
}
private void validateAuthorizationDetail(AuthorizationDetail detail, int index, Errors errors) {
if (detail.getType() == null || detail.getType().trim().isEmpty()) {
errors.rejectValue("authorizationDetails[" + index + "].type",
ValidationError.MISSING_TYPE, "Authorization detail type is required");
return;
}
// Type-specific validation
switch (detail.getType()) {
case "payment":
validatePaymentDetail((PaymentAuthorizationDetail) detail, index, errors);
break;
case "account_information":
validateAccountInformationDetail((AccountInformationAuthorizationDetail) detail, index, errors);
break;
case "document_access":
validateDocumentAccessDetail((DocumentAccessAuthorizationDetail) detail, index, errors);
break;
case "location":
validateLocationDetail((LocationAuthorizationDetail) detail, index, errors);
break;
case "custom":
validateCustomDetail((CustomAuthorizationDetail) detail, index, errors);
break;
default:
errors.rejectValue("authorizationDetails[" + index + "].type",
ValidationError.INVALID_TYPE, "Unsupported authorization type: " + detail.getType());
}
}
private void validatePaymentDetail(PaymentAuthorizationDetail detail, int index, Errors errors) {
// Validate debtor account
if (detail.getDebtorAccount() == null || detail.getDebtorAccount().trim().isEmpty()) {
errors.rejectValue("authorizationDetails[" + index + "].debtorAccount",
ValidationError.MISSING_DEBTOR_ACCOUNT, "Debtor account is required");
} else if (!IBAN_PATTERN.matcher(detail.getDebtorAccount()).matches()) {
errors.rejectValue("authorizationDetails[" + index + "].debtorAccount",
ValidationError.INVALID_DEBTOR_ACCOUNT, "Invalid debtor account format");
}
// Validate creditor account
if (detail.getCreditorAccount() == null || detail.getCreditorAccount().trim().isEmpty()) {
errors.rejectValue("authorizationDetails[" + index + "].creditorAccount",
ValidationError.MISSING_CREDITOR_ACCOUNT, "Creditor account is required");
} else if (!IBAN_PATTERN.matcher(detail.getCreditorAccount()).matches()) {
errors.rejectValue("authorizationDetails[" + index + "].creditorAccount",
ValidationError.INVALID_CREDITOR_ACCOUNT, "Invalid creditor account format");
}
// Validate amount
if (detail.getAmount() == null) {
errors.rejectValue("authorizationDetails[" + index + "].amount",
ValidationError.MISSING_AMOUNT, "Amount is required");
} else if (detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
errors.rejectValue("authorizationDetails[" + index + "].amount",
ValidationError.INVALID_AMOUNT, "Amount must be positive");
}
// Validate recurring details if applicable
if (detail.getRecurringPayment() != null && detail.getRecurringPayment()) {
validateRecurringDetails(detail.getRecurringDetails(), index, errors);
}
// Validate BIC if provided
if (detail.getCreditorAgent() != null && !detail.getCreditorAgent().isEmpty() &&
!BIC_PATTERN.matcher(detail.getCreditorAgent()).matches()) {
errors.rejectValue("authorizationDetails[" + index + "].creditorAgent",
"payment.creditor_agent.invalid", "Invalid BIC format");
}
}
private void validateAccountInformationDetail(AccountInformationAuthorizationDetail detail, int index, Errors errors) {
// Validate account IDs
if (detail.getAccountIds() == null || detail.getAccountIds().isEmpty()) {
errors.rejectValue("authorizationDetails[" + index + "].accountIds",
ValidationError.MISSING_ACCOUNT_IDS, "At least one account ID is required");
}
// Validate permissions
if (detail.getPermissions() != null) {
for (AccountInformationAuthorizationDetail.AccountPermission permission : detail.getPermissions()) {
if (permission == null) {
errors.rejectValue("authorizationDetails[" + index + "].permissions",
ValidationError.INVALID_PERMISSION, "Invalid permission");
}
}
}
// Validate date range if provided
if (detail.getTransactionFrom() != null && detail.getTransactionTo() != null &&
detail.getTransactionFrom().isAfter(detail.getTransactionTo())) {
errors.rejectValue("authorizationDetails[" + index + "].transactionFrom",
"account_information.date_range.invalid", "From date must be before to date");
}
}
private void validateDocumentAccessDetail(DocumentAccessAuthorizationDetail detail, int index, Errors errors) {
// Validate document IDs or types
if ((detail.getDocumentIds() == null || detail.getDocumentIds().isEmpty()) &&
(detail.getDocumentTypes() == null || detail.getDocumentTypes().isEmpty())) {
errors.rejectValue("authorizationDetails[" + index + "].documentIds",
ValidationError.MISSING_DOCUMENT_IDS, "Either document IDs or types must be specified");
}
// Validate permission
if (detail.getPermission() == null) {
errors.rejectValue("authorizationDetails[" + index + "].permission",
"document_access.permission.missing", "Document permission is required");
}
// Validate expiration
if (detail.getAccessUntil() != null && detail.getAccessUntil().isBefore(LocalDateTime.now())) {
errors.rejectValue("authorizationDetails[" + index + "].accessUntil",
"document_access.expired", "Access until date must be in the future");
}
}
private void validateLocationDetail(LocationAuthorizationDetail detail, int index, Errors errors) {
// Validate precision
if (detail.getPrecision() == null) {
detail.setPrecision(LocationAuthorizationDetail.LocationPrecision.APPROXIMATE);
}
// Validate coordinates if provided
if (detail.getAllowedAreas() != null) {
for (LocationAuthorizationDetail.LocationCoordinates coord : detail.getAllowedAreas()) {
if (coord.getLatitude() < -90 || coord.getLatitude() > 90 ||
coord.getLongitude() < -180 || coord.getLongitude() > 180) {
errors.rejectValue("authorizationDetails[" + index + "].allowedAreas",
ValidationError.INVALID_LOCATION, "Invalid coordinates");
}
}
}
// Validate validUntil
if (detail.getValidUntil() != null && detail.getValidUntil().isBefore(LocalDateTime.now())) {
errors.rejectValue("authorizationDetails[" + index + "].validUntil",
"location.expired", "Valid until date must be in the future");
}
}
private void validateCustomDetail(CustomAuthorizationDetail detail, int index, Errors errors) {
// Custom validation logic based on specific requirements
if (detail.getCustomParameters() == null || detail.getCustomParameters().isEmpty()) {
errors.rejectValue("authorizationDetails[" + index + "].customParameters",
"custom.parameters.missing", "Custom parameters are required");
}
}
private void validateRecurringDetails(PaymentAuthorizationDetail.RecurringDetails details, int index, Errors errors) {
if (details == null) {
errors.rejectValue("authorizationDetails[" + index + "].recurringDetails",
"payment.recurring_details.missing", "Recurring details are required for recurring payments");
return;
}
if (details.getFrequency() == null || details.getFrequency().trim().isEmpty()) {
errors.rejectValue("authorizationDetails[" + index + "].recurringDetails.frequency",
"payment.recurring_details.frequency.missing", "Frequency is required");
}
if (details.getMaximumAmount() != null && details.getMaximumAmount().compareTo(BigDecimal.ZERO) <= 0) {
errors.rejectValue("authorizationDetails[" + index + "].recurringDetails.maximumAmount",
"payment.recurring_details.maximum_amount.invalid", "Maximum amount must be positive");
}
}
/**
* Validate JSON string representation of RAR
*/
public List<String> validateJson(String jsonRar) {
List<String> errors = new ArrayList<>();
try {
// Parse JSON and validate structure
Object document = JsonPath.parse(jsonRar);
// Check required fields
checkRequiredField(document, "$.client_id", errors);
checkRequiredField(document, "$.response_type", errors);
// Validate authorization_details array
List<Map<String, Object>> details = JsonPath.read(jsonRar, "$.authorization_details");
if (details == null || details.isEmpty()) {
errors.add("authorization_details array is required and cannot be empty");
} else {
for (int i = 0; i < details.size(); i++) {
validateJsonDetail(jsonRar, i, errors);
}
}
} catch (Exception e) {
errors.add("Invalid JSON format: " + e.getMessage());
}
return errors;
}
private void checkRequiredField(Object document, String path, List<String> errors) {
try {
Object value = JsonPath.read(document, path);
if (value == null || (value instanceof String && ((String) value).isEmpty())) {
errors.add("Required field missing: " + path);
}
} catch (PathNotFoundException e) {
errors.add("Required field missing: " + path);
}
}
private void validateJsonDetail(String json, int index, List<String> errors) {
try {
String type = JsonPath.read(json, "$.authorization_details[" + index + "].type");
if (type == null || type.isEmpty()) {
errors.add("authorization_details[" + index + "].type is required");
return;
}
switch (type) {
case "payment":
validateJsonPaymentDetail(json, index, errors);
break;
case "account_information":
validateJsonAccountDetail(json, index, errors);
break;
case "document_access":
validateJsonDocumentDetail(json, index, errors);
break;
case "location":
validateJsonLocationDetail(json, index, errors);
break;
default:
errors.add("authorization_details[" + index + "].type: Unsupported type '" + type + "'");
}
} catch (PathNotFoundException e) {
errors.add("authorization_details[" + index + "]: Missing type field");
}
}
private void validateJsonPaymentDetail(String json, int index, List<String> errors) {
// Validate payment-specific fields
checkJsonField(json, "$.authorization_details[" + index + "].debtor_account", errors);
checkJsonField(json, "$.authorization_details[" + index + "].creditor_account", errors);
checkJsonField(json, "$.authorization_details[" + index + "].amount", errors);
checkJsonField(json, "$.authorization_details[" + index + "].currency", errors);
}
private void validateJsonAccountDetail(String json, int index, List<String> errors) {
// Validate account information fields
checkJsonField(json, "$.authorization_details[" + index + "].account_ids", errors);
}
private void validateJsonDocumentDetail(String json, int index, List<String> errors) {
// Validate document access fields
checkJsonField(json, "$.authorization_details[" + index + "].permission", errors);
}
private void validateJsonLocationDetail(String json, int index, List<String> errors) {
// Validate location fields
// Optional fields, no required checks
}
private void checkJsonField(String json, String path, List<String> errors) {
try {
Object value = JsonPath.read(json, path);
if (value == null) {
errors.add("Required field missing: " + path);
}
} catch (PathNotFoundException e) {
errors.add("Required field missing: " + path);
}
}
}
4. RAR Authorization Server
package com.oauth.rar.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oauth.rar.model.*;
import com.oauth.rar.validator.RARRequestValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class RARAuthorizationServer {
private final RARRequestValidator validator;
private final ObjectMapper objectMapper;
// In-memory storage (use database in production)
private final Map<String, AuthorizationRequest> requestStore = new ConcurrentHashMap<>();
private final Map<String, AuthorizationResponse> responseStore = new ConcurrentHashMap<>();
private final Map<String, ClientRegistration> clientStore = new ConcurrentHashMap<>();
public RARAuthorizationServer(RARRequestValidator validator) {
this.validator = validator;
this.objectMapper = new ObjectMapper();
initializeClients();
}
/**
* Client registration
*/
@Data
@Builder
public static class ClientRegistration {
private String clientId;
private String clientSecret;
private Set<String> redirectUris;
private Set<String> allowedAuthorizationTypes;
private Map<String, Object> metadata;
}
/**
* Authorization request (pending user consent)
*/
@Data
@Builder
public static class AuthorizationRequest {
private String requestId;
private String clientId;
private RichAuthorizationRequest request;
private LocalDateTime createdAt;
private LocalDateTime expiresAt;
private String state;
private Map<String, Boolean> consentStatus; // detail index -> granted
private Map<String, Object> userInput; // user-provided data for consent
}
/**
* Process rich authorization request
*/
public AuthorizationResponse processAuthorizationRequest(RichAuthorizationRequest request) {
// Validate client
ClientRegistration client = clientStore.get(request.getClientId());
if (client == null) {
throw new IllegalArgumentException("Unknown client: " + request.getClientId());
}
// Validate redirect URI
if (request.getRedirectUri() != null &&
!client.getRedirectUris().contains(request.getRedirectUri())) {
throw new IllegalArgumentException("Invalid redirect URI");
}
// Create authorization request (pending user consent)
String requestId = generateRequestId();
AuthorizationRequest authRequest = AuthorizationRequest.builder()
.requestId(requestId)
.clientId(request.getClientId())
.request(request)
.createdAt(LocalDateTime.now())
.expiresAt(LocalDateTime.now().plusMinutes(10))
.state(request.getState())
.consentStatus(new HashMap<>())
.userInput(new HashMap<>())
.build();
requestStore.put(requestId, authRequest);
// Return authorization response (redirect to consent page)
return AuthorizationResponse.builder()
.code(requestId) // Temporary code for consent UI
.state(request.getState())
.build();
}
/**
* Get authorization request details for consent UI
*/
public AuthorizationRequest getAuthorizationRequest(String requestId) {
AuthorizationRequest authRequest = requestStore.get(requestId);
if (authRequest == null || authRequest.getExpiresAt().isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("Authorization request expired or not found");
}
return authRequest;
}
/**
* Approve authorization request (user consent)
*/
public AuthorizationResponse approveRequest(String requestId, Map<String, Boolean> consent, Map<String, Object> userInput) {
AuthorizationRequest authRequest = getAuthorizationRequest(requestId);
// Update consent status
authRequest.setConsentStatus(consent);
authRequest.setUserInput(userInput);
// Generate authorization code
String authCode = generateAuthorizationCode();
// Create authorized details based on consent
List<AuthorizedDetail> authorizedDetails = createAuthorizedDetails(authRequest);
AuthorizationResponse response = AuthorizationResponse.builder()
.code(authCode)
.state(authRequest.getState())
.authorizedDetails(authorizedDetails)
.additionalData(userInput)
.build();
responseStore.put(authCode, response);
// Clean up request
requestStore.remove(requestId);
return response;
}
/**
* Deny authorization request
*/
public AuthorizationResponse denyRequest(String requestId, String reason) {
AuthorizationRequest authRequest = requestStore.remove(requestId);
return AuthorizationResponse.builder()
.state(authRequest != null ? authRequest.getState() : null)
.additionalData(Map.of("error", "access_denied", "error_description", reason))
.build();
}
/**
* Exchange authorization code for token with RAR details
*/
public RARAccessToken exchangeCodeForToken(String code, String clientId, String clientSecret) {
// Validate client
ClientRegistration client = clientStore.get(clientId);
if (client == null || !client.getClientSecret().equals(clientSecret)) {
throw new IllegalArgumentException("Invalid client credentials");
}
// Get authorization response
AuthorizationResponse response = responseStore.get(code);
if (response == null) {
throw new IllegalArgumentException("Invalid authorization code");
}
// Create token with embedded RAR details
RARAccessToken token = RARAccessToken.builder()
.tokenValue(generateTokenValue())
.tokenType("Bearer")
.expiresIn(3600L)
.scope(determineScope(response))
.authorizationDetails(response.getAuthorizedDetails())
.additionalClaims(Map.of(
"client_id", clientId,
"issued_at", System.currentTimeMillis() / 1000
))
.build();
// Clean up used code
responseStore.remove(code);
return token;
}
/**
* Introspect token (for resource servers)
*/
public Map<String, Object> introspectToken(String token) {
// In production, validate token signature and expiration
// For demo, return mock introspection response
RARAccessToken rarToken = parseToken(token);
if (rarToken == null) {
return Map.of("active", false);
}
Map<String, Object> introspection = new HashMap<>();
introspection.put("active", true);
introspection.put("client_id", "client123");
introspection.put("exp", System.currentTimeMillis() / 1000 + 3600);
introspection.put("scope", rarToken.getScope());
// Include authorization details in introspection response
if (rarToken.getAuthorizationDetails() != null) {
introspection.put("authorization_details", rarToken.getAuthorizationDetails());
}
return introspection;
}
/**
* Register a new client
*/
public void registerClient(ClientRegistration client) {
clientStore.put(client.getClientId(), client);
log.info("Client registered: {}", client.getClientId());
}
private List<AuthorizedDetail> createAuthorizedDetails(AuthorizationRequest authRequest) {
List<AuthorizedDetail> details = new ArrayList<>();
for (int i = 0; i < authRequest.getRequest().getAuthorizationDetails().size(); i++) {
AuthorizationDetail original = authRequest.getRequest().getAuthorizationDetails().get(i);
Boolean granted = authRequest.getConsentStatus().get(String.valueOf(i));
if (granted != null && granted) {
AuthorizedDetail detail = new AuthorizedDetail();
detail.setType(original.getType());
// Add type-specific data based on consent
Map<String, Object> authorizedData = new HashMap<>();
if (original instanceof PaymentAuthorizationDetail) {
PaymentAuthorizationDetail payment = (PaymentAuthorizationDetail) original;
authorizedData.put("payment_type", payment.getPaymentType());
authorizedData.put("currency", payment.getCurrency());
authorizedData.put("amount", payment.getAmount());
// Don't include full account details in token
} else if (original instanceof AccountInformationAuthorizationDetail) {
AccountInformationAuthorizationDetail account = (AccountInformationAuthorizationDetail) original;
authorizedData.put("permissions", account.getPermissions());
authorizedData.put("include_balance", account.getIncludeBalance());
}
detail.setAuthorizedData(authorizedData);
// Add user input if provided
if (authRequest.getUserInput() != null) {
detail.setConstraints(authRequest.getUserInput());
}
details.add(detail);
}
}
return details;
}
private String determineScope(AuthorizationResponse response) {
Set<String> scopes = new HashSet<>();
if (response.getAuthorizedDetails() != null) {
for (AuthorizedDetail detail : response.getAuthorizedDetails()) {
switch (detail.getType()) {
case "payment":
scopes.add("payment");
break;
case "account_information":
scopes.add("accounts");
break;
case "document_access":
scopes.add("documents");
break;
case "location":
scopes.add("location");
break;
}
}
}
return String.join(" ", scopes);
}
private RARAccessToken parseToken(String token) {
// In production, validate JWT signature
// For demo, parse mock token
return null;
}
private String generateRequestId() {
return "REQ-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
private String generateAuthorizationCode() {
return "AUTH-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
private String generateTokenValue() {
return "TOKEN-" + UUID.randomUUID().toString();
}
private void initializeClients() {
// Demo client
registerClient(ClientRegistration.builder()
.clientId("demo-client")
.clientSecret("demo-secret")
.redirectUris(Set.of("https://client.example.com/callback"))
.allowedAuthorizationTypes(Set.of("payment", "account_information", "document_access", "location"))
.build());
}
}
5. RAR Token Service
package com.oauth.rar.token;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.oauth.rar.model.AuthorizedDetail;
import com.oauth.rar.model.RARAccessToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Service
public class RARTokenService {
private final RSAKey rsaKey;
private final RSAPrivateKey privateKey;
private final RSAPublicKey publicKey;
private final ObjectMapper objectMapper;
public RARTokenService() throws Exception {
// Generate RSA key pair for JWT signing
this.rsaKey = new RSAKeyGenerator(2048)
.keyID(UUID.randomUUID().toString())
.generate();
this.privateKey = rsaKey.toRSAPrivateKey();
this.publicKey = rsaKey.toRSAPublicKey();
this.objectMapper = new ObjectMapper();
}
/**
* Create RAR JWT token with embedded authorization details
*/
public String createRARToken(RARAccessToken token) throws JOSEException, JsonProcessingException {
Instant now = Instant.now();
Instant expiry = now.plusSeconds(token.getExpiresIn());
// Build claims
JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
.jwtID(UUID.randomUUID().toString())
.issuer("https://auth.example.com")
.subject(token.getTokenValue())
.issueTime(Date.from(now))
.expirationTime(Date.from(expiry))
.claim("client_id", token.getAdditionalClaims().get("client_id"))
.claim("scope", token.getScope());
// Add authorization details if present
if (token.getAuthorizationDetails() != null && !token.getAuthorizationDetails().isEmpty()) {
String authDetailsJson = objectMapper.writeValueAsString(token.getAuthorizationDetails());
claimsBuilder.claim("authorization_details", authDetailsJson);
}
// Add any additional claims
if (token.getAdditionalClaims() != null) {
token.getAdditionalClaims().forEach(claimsBuilder::claim);
}
JWTClaimsSet claimsSet = claimsBuilder.build();
// Sign the JWT
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(rsaKey.getKeyID())
.type(new JOSEObjectType("rar+jwt"))
.build(),
claimsSet
);
JWSSigner signer = new RSASSASigner(privateKey);
signedJWT.sign(signer);
return signedJWT.serialize();
}
/**
* Parse and validate RAR token
*/
public RARAccessToken parseAndValidateToken(String tokenString) throws Exception {
SignedJWT signedJWT = SignedJWT.parse(tokenString);
// Verify signature
JWSVerifier verifier = new RSASSAVerifier(publicKey);
if (!signedJWT.verify(verifier)) {
throw new SecurityException("Invalid token signature");
}
// Validate expiration
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
if (claims.getExpirationTime().before(new Date())) {
throw new SecurityException("Token expired");
}
// Build RAR token object
RARAccessToken.RARAccessTokenBuilder builder = RARAccessToken.builder()
.tokenValue(claims.getSubject())
.tokenType("Bearer")
.expiresIn((claims.getExpirationTime().getTime() - System.currentTimeMillis()) / 1000)
.scope(claims.getStringClaim("scope"));
// Extract authorization details
String authDetailsJson = claims.getStringClaim("authorization_details");
if (authDetailsJson != null) {
List<AuthorizedDetail> authDetails = objectMapper.readValue(
authDetailsJson,
objectMapper.getTypeFactory().constructCollectionType(List.class, AuthorizedDetail.class)
);
builder.authorizationDetails(authDetails);
}
// Extract additional claims
Map<String, Object> additionalClaims = claims.getClaims();
additionalClaims.remove("authorization_details");
additionalClaims.remove("scope");
additionalClaims.remove("client_id");
builder.additionalClaims(additionalClaims);
return builder.build();
}
/**
* Extract authorization details from token
*/
public List<AuthorizedDetail> extractAuthorizationDetails(String tokenString) throws Exception {
RARAccessToken token = parseAndValidateToken(tokenString);
return token.getAuthorizationDetails();
}
/**
* Check if token has specific authorization
*/
public boolean hasAuthorization(String tokenString, String type, String action) throws Exception {
List<AuthorizedDetail> details = extractAuthorizationDetails(tokenString);
if (details == null) return false;
return details.stream()
.filter(d -> d.getType().equals(type))
.anyMatch(d -> {
if (d.getGrantedActions() != null) {
return d.getGrantedActions().contains(action);
}
return false;
});
}
/**
* Get public key for token verification (for resource servers)
*/
public RSAKey getPublicKey() {
return rsaKey.toPublicJWK();
}
}
6. RAR Resource Server Integration
package com.oauth.rar.resource;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import com.oauth.rar.model.AuthorizedDetail;
import com.oauth.rar.token.RARTokenService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class RARAuthorizationInterceptor implements HandlerInterceptor {
private final RARTokenService tokenService;
private final ObjectMapper objectMapper;
public RARAuthorizationInterceptor(RARTokenService tokenService) {
this.tokenService = tokenService;
this.objectMapper = new ObjectMapper();
}
@Override
public boolean preHandle(HttpServletRequest request, Object handler, Exception ex) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod method = (HandlerMethod) handler;
RequireRAR rarAnnotation = method.getMethodAnnotation(RequireRAR.class);
if (rarAnnotation == null) {
return true; // No RAR requirement
}
// Extract token from Authorization header
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new SecurityException("Missing or invalid Authorization header");
}
String token = authHeader.substring(7);
try {
// Validate token and extract RAR details
List<AuthorizedDetail> authDetails = tokenService.extractAuthorizationDetails(token);
// Check required authorization
if (!hasRequiredAuthorization(authDetails, rarAnnotation)) {
throw new SecurityException("Insufficient authorization");
}
// Add RAR details to request attributes for controller use
request.setAttribute("rar.authorization_details", authDetails);
// Extract specific data based on annotations
extractRARData(request, authDetails);
return true;
} catch (Exception e) {
log.error("RAR authorization failed", e);
throw new SecurityException("Authorization failed: " + e.getMessage());
}
}
private boolean hasRequiredAuthorization(List<AuthorizedDetail> details, RequireRAR annotation) {
String requiredType = annotation.type();
String requiredAction = annotation.action();
if (details == null) return false;
return details.stream()
.filter(d -> d.getType().equals(requiredType))
.anyMatch(d -> {
if (d.getGrantedActions() != null) {
return d.getGrantedActions().contains(requiredAction);
}
return false;
});
}
private void extractRARData(HttpServletRequest request, List<AuthorizedDetail> details) {
// Extract payment data
details.stream()
.filter(d -> "payment".equals(d.getType()))
.findFirst()
.ifPresent(d -> {
Map<String, Object> paymentData = d.getAuthorizedData();
request.setAttribute("rar.payment", paymentData);
});
// Extract account data
details.stream()
.filter(d -> "account_information".equals(d.getType()))
.findFirst()
.ifPresent(d -> {
Map<String, Object> accountData = d.getAuthorizedData();
request.setAttribute("rar.accounts", accountData);
});
// Extract document data
details.stream()
.filter(d -> "document_access".equals(d.getType()))
.findFirst()
.ifPresent(d -> {
Map<String, Object> documentData = d.getAuthorizedData();
request.setAttribute("rar.documents", documentData);
});
}
}
/**
* Annotation for requiring specific RAR authorization
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRAR {
String type();
String action() default "";
}
/**
* RAR-aware resource controller
*/
@RestController
@RequestMapping("/api/resources")
public class RARProtectedResourceController {
@GetMapping("/payments")
@RequireRAR(type = "payment", action = "READ")
public ResponseEntity<?> getPayments(HttpServletRequest request) {
Map<String, Object> paymentData = (Map<String, Object>) request.getAttribute("rar.payment");
// Process request based on authorized payment details
return ResponseEntity.ok(Map.of(
"message", "Payments retrieved",
"authorized_data", paymentData
));
}
@PostMapping("/payments")
@RequireRAR(type = "payment", action = "CREATE")
public ResponseEntity<?> createPayment(@RequestBody PaymentRequest payment,
HttpServletRequest request) {
Map<String, Object> paymentAuth = (Map<String, Object>) request.getAttribute("rar.payment");
// Validate payment against authorized limits
// Process payment
return ResponseEntity.ok(Map.of(
"message", "Payment created",
"payment_id", UUID.randomUUID().toString()
));
}
@GetMapping("/accounts")
@RequireRAR(type = "account_information", action = "READ")
public ResponseEntity<?> getAccounts(HttpServletRequest request) {
Map<String, Object> accountData = (Map<String, Object>) request.getAttribute("rar.accounts");
// Return accounts based on authorized permissions
return ResponseEntity.ok(accountData);
}
@GetMapping("/documents/{documentId}")
@RequireRAR(type = "document_access", action = "READ")
public ResponseEntity<?> getDocument(@PathVariable String documentId,
HttpServletRequest request) {
Map<String, Object> documentAuth = (Map<String, Object>) request.getAttribute("rar.documents");
// Check if document is authorized
// Return document
return ResponseEntity.ok(Map.of(
"document_id", documentId,
"content", "Sample document content"
));
}
}
7. RAR REST Controllers
package com.oauth.rar.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oauth.rar.model.*;
import com.oauth.rar.server.RARAuthorizationServer;
import com.oauth.rar.token.RARTokenService;
import com.oauth.rar.validator.RARRequestValidator;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor
public class RARController {
private final RARAuthorizationServer authServer;
private final RARTokenService tokenService;
private final RARRequestValidator validator;
private final ObjectMapper objectMapper;
/**
* Authorization endpoint with RAR support
*/
@GetMapping("/authorize")
public ResponseEntity<?> authorize(
@RequestParam String client_id,
@RequestParam String response_type,
@RequestParam(required = false) String redirect_uri,
@RequestParam(required = false) String scope,
@RequestParam(required = false) String state,
@RequestParam(required = false) String authorization_details) {
try {
// Parse authorization_details if provided
List<AuthorizationDetail> details = null;
if (authorization_details != null) {
details = objectMapper.readValue(
authorization_details,
objectMapper.getTypeFactory().constructCollectionType(List.class, AuthorizationDetail.class)
);
}
// Build RAR request
RichAuthorizationRequest request = RichAuthorizationRequest.builder()
.clientId(client_id)
.responseType(response_type)
.redirectUri(redirect_uri)
.scope(scope)
.state(state)
.authorizationDetails(details)
.build();
// Validate request
var errors = validator.validateJson(authorization_details);
if (!errors.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"error", "invalid_request",
"error_description", String.join(", ", errors)
));
}
// Process authorization request
AuthorizationResponse response = authServer.processAuthorizationRequest(request);
// Redirect to consent page
return ResponseEntity.ok(Map.of(
"authorization_request_id", response.getCode(),
"state", response.getState()
));
} catch (JsonProcessingException e) {
return ResponseEntity.badRequest().body(Map.of(
"error", "invalid_request",
"error_description", "Invalid authorization_details format"
));
}
}
/**
* Consent page - get authorization details
*/
@GetMapping("/consent/{requestId}")
public ResponseEntity<?> getConsentDetails(@PathVariable String requestId) {
try {
RARAuthorizationServer.AuthorizationRequest request =
authServer.getAuthorizationRequest(requestId);
return ResponseEntity.ok(Map.of(
"request_id", request.getRequestId(),
"client_id", request.getClientId(),
"authorization_details", request.getRequest().getAuthorizationDetails()
));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of(
"error", "invalid_request",
"error_description", e.getMessage()
));
}
}
/**
* Consent approval
*/
@PostMapping("/consent/{requestId}/approve")
public ResponseEntity<?> approveConsent(
@PathVariable String requestId,
@RequestBody ConsentApproval approval) {
AuthorizationResponse response = authServer.approveRequest(
requestId,
approval.getConsent(),
approval.getUserInput()
);
// Redirect to client with authorization code
return ResponseEntity.ok(response);
}
/**
* Consent denial
*/
@PostMapping("/consent/{requestId}/deny")
public ResponseEntity<?> denyConsent(
@PathVariable String requestId,
@RequestParam(required = false) String reason) {
AuthorizationResponse response = authServer.denyRequest(requestId, reason);
return ResponseEntity.ok(response);
}
/**
* Token endpoint - exchange code for token with RAR details
*/
@PostMapping("/token")
public ResponseEntity<?> token(
@RequestParam String grant_type,
@RequestParam String code,
@RequestParam String client_id,
@RequestParam String client_secret) {
if (!"authorization_code".equals(grant_type)) {
return ResponseEntity.badRequest().body(Map.of(
"error", "unsupported_grant_type"
));
}
try {
RARAccessToken token = authServer.exchangeCodeForToken(
code, client_id, client_secret
);
// Create JWT token with embedded RAR details
String jwtToken = tokenService.createRARToken(token);
return ResponseEntity.ok(Map.of(
"access_token", jwtToken,
"token_type", token.getTokenType(),
"expires_in", token.getExpiresIn(),
"scope", token.getScope()
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"error", "invalid_grant",
"error_description", e.getMessage()
));
}
}
/**
* Token introspection endpoint (RFC 7662)
*/
@PostMapping("/introspect")
public ResponseEntity<?> introspect(@RequestParam String token) {
Map<String, Object> introspection = authServer.introspectToken(token);
return ResponseEntity.ok(introspection);
}
/**
* JWKS endpoint for token verification
*/
@GetMapping("/jwks")
public ResponseEntity<?> jwks() {
return ResponseEntity.ok(Map.of(
"keys", List.of(tokenService.getPublicKey().toJSONObject())
));
}
/**
* Consent approval DTO
*/
public static class ConsentApproval {
private Map<String, Boolean> consent;
private Map<String, Object> userInput;
// getters and setters
}
}
8. RAR Client Library
package com.oauth.rar.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oauth.rar.model.AuthorizationDetail;
import com.oauth.rar.model.RichAuthorizationRequest;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Slf4j
public class RARClient {
private final String authServerUrl;
private final String clientId;
private final String clientSecret;
private final String redirectUri;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
@Builder
public RARClient(String authServerUrl, String clientId, String clientSecret, String redirectUri) {
this.authServerUrl = authServerUrl;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectUri = redirectUri;
this.restTemplate = new RestTemplate();
this.objectMapper = new ObjectMapper();
}
/**
* Build authorization URL with RAR
*/
public String buildAuthorizationUrl(List<AuthorizationDetail> details, String state) {
try {
String authDetailsJson = objectMapper.writeValueAsString(details);
return String.format(
"%s/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&authorization_details=%s&state=%s",
authServerUrl,
clientId,
redirectUri,
authDetailsJson,
state
);
} catch (Exception e) {
throw new RuntimeException("Failed to build authorization URL", e);
}
}
/**
* Exchange authorization code for token
*/
public RARTokenResponse exchangeCodeForToken(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
String body = String.format(
"grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s&redirect_uri=%s",
code, clientId, clientSecret, redirectUri
);
HttpEntity<String> request = new HttpEntity<>(body, headers);
ResponseEntity<RARTokenResponse> response = restTemplate.postForEntity(
authServerUrl + "/oauth/token",
request,
RARTokenResponse.class
);
return response.getBody();
}
/**
* Introspect token
*/
public TokenIntrospectionResponse introspectToken(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
String body = String.format(
"token=%s&client_id=%s&client_secret=%s",
token, clientId, clientSecret
);
HttpEntity<String> request = new HttpEntity<>(body, headers);
ResponseEntity<TokenIntrospectionResponse> response = restTemplate.postForEntity(
authServerUrl + "/oauth/introspect",
request,
TokenIntrospectionResponse.class
);
return response.getBody();
}
/**
* Call protected resource with token
*/
public <T> T callProtectedResource(String resourceUrl, String token, Class<T> responseType) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<T> response = restTemplate.exchange(
resourceUrl,
HttpMethod.GET,
request,
responseType
);
return response.getBody();
}
/**
* RAR token response
*/
@Data
public static class RARTokenResponse {
private String access_token;
private String token_type;
private Long expires_in;
private String scope;
private List<Map<String, Object>> authorization_details;
}
/**
* Token introspection response
*/
@Data
public static class TokenIntrospectionResponse {
private boolean active;
private String client_id;
private Long exp;
private String scope;
private List<Map<String, Object>> authorization_details;
}
}
9. RAR Examples and Usage
package com.oauth.rar.examples;
import com.oauth.rar.client.RARClient;
import com.oauth.rar.model.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
public class RARUsageExamples {
public static void main(String[] args) {
// Initialize client
RARClient client = RARClient.builder()
.authServerUrl("https://auth.example.com")
.clientId("payment-app")
.clientSecret("secret")
.redirectUri("https://app.example.com/callback")
.build();
// Example 1: Payment authorization
examplePaymentAuthorization(client);
// Example 2: Account information access
exampleAccountInformation(client);
// Example 3: Document access with restrictions
exampleDocumentAccess(client);
// Example 4: Location access with precision
exampleLocationAccess(client);
// Example 5: Multi-type authorization
exampleMultiTypeAuthorization(client);
}
/**
* Payment authorization example
*/
public static void examplePaymentAuthorization(RARClient client) {
// Create payment authorization detail
PaymentAuthorizationDetail paymentDetail = PaymentAuthorizationDetail.builder()
.type("payment")
.paymentType(PaymentAuthorizationDetail.PaymentType.SINGLE)
.debtorAccount("DE89370400440532013000")
.creditorAccount("FR7630006000011234567890189")
.creditorName("Example Merchant")
.amount(new BigDecimal("99.99"))
.currency("EUR")
.remittanceInformation("Order #12345")
.build();
// Build authorization URL
String authUrl = client.buildAuthorizationUrl(
List.of(paymentDetail),
"state-123"
);
System.out.println("1. Redirect user to: " + authUrl);
System.out.println("2. User approves payment consent");
System.out.println("3. Exchange code for token");
}
/**
* Account information access example
*/
public static void exampleAccountInformation(RARClient client) {
// Create account information authorization
AccountInformationAuthorizationDetail accountDetail = AccountInformationAuthorizationDetail.builder()
.type("account_information")
.accountIds(List.of("DE89370400440532013000", "DE89370400440532013001"))
.permissions(List.of(
AccountInformationAuthorizationDetail.AccountPermission.VIEW_BALANCE,
AccountInformationAuthorizationDetail.AccountPermission.VIEW_TRANSACTIONS
))
.includeBalance(true)
.includeTransactions(true)
.transactionFrom(LocalDateTime.now().minusMonths(3))
.transactionTo(LocalDateTime.now())
.recurringAccess(true)
.build();
String authUrl = client.buildAuthorizationUrl(
List.of(accountDetail),
"state-456"
);
System.out.println("Account access URL: " + authUrl);
}
/**
* Document access with restrictions example
*/
public static void exampleDocumentAccess(RARClient client) {
// Create document access authorization
DocumentAccessAuthorizationDetail documentDetail = DocumentAccessAuthorizationDetail.builder()
.type("document_access")
.documentIds(List.of("DOC-2024-001", "DOC-2024-002"))
.documentTypes(List.of(
DocumentAccessAuthorizationDetail.DocumentType.STATEMENT,
DocumentAccessAuthorizationDetail.DocumentType.INVOICE
))
.permission(DocumentAccessAuthorizationDetail.DocumentPermission.READ)
.allowDownload(true)
.allowPrint(false)
.accessUntil(LocalDateTime.now().plusDays(30))
.build();
String authUrl = client.buildAuthorizationUrl(
List.of(documentDetail),
"state-789"
);
System.out.println("Document access URL: " + authUrl);
}
/**
* Location access with precision example
*/
public static void exampleLocationAccess(RARClient client) {
// Create location authorization
LocationAuthorizationDetail.LocationCoordinates area =
LocationAuthorizationDetail.LocationCoordinates.builder()
.latitude(48.8566)
.longitude(2.3522)
.radius(1000) // 1km radius
.build();
LocationAuthorizationDetail locationDetail = LocationAuthorizationDetail.builder()
.type("location")
.precision(LocationAuthorizationDetail.LocationPrecision.APPROXIMATE)
.allowedAreas(List.of(area))
.maxUpdates(1000)
.validUntil(LocalDateTime.now().plusDays(7))
.backgroundTracking(false)
.build();
String authUrl = client.buildAuthorizationUrl(
List.of(locationDetail),
"state-loc-123"
);
System.out.println("Location access URL: " + authUrl);
}
/**
* Multi-type authorization example
*/
public static void exampleMultiTypeAuthorization(RARClient client) {
// Payment detail
PaymentAuthorizationDetail paymentDetail = PaymentAuthorizationDetail.builder()
.type("payment")
.paymentType(PaymentAuthorizationDetail.PaymentType.SINGLE)
.debtorAccount("DE89370400440532013000")
.creditorAccount("FR7630006000011234567890189")
.amount(new BigDecimal("49.99"))
.currency("EUR")
.build();
// Account information detail
AccountInformationAuthorizationDetail accountDetail = AccountInformationAuthorizationDetail.builder()
.type("account_information")
.accountIds(List.of("DE89370400440532013000"))
.permissions(List.of(
AccountInformationAuthorizationDetail.AccountPermission.VIEW_BALANCE
))
.includeBalance(true)
.build();
// Combine multiple authorization types
String authUrl = client.buildAuthorizationUrl(
List.of(paymentDetail, accountDetail),
"state-multi-123"
);
System.out.println("Multi-type authorization URL: " + authUrl);
// After user consent, exchange code for token
// The resulting token will contain both authorizations
}
}
10. Testing RAR Implementation
package com.oauth.rar.test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oauth.rar.model.*;
import com.oauth.rar.server.RARAuthorizationServer;
import com.oauth.rar.token.RARTokenService;
import com.oauth.rar.validator.RARRequestValidator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class RARTest {
private RARAuthorizationServer authServer;
private RARTokenService tokenService;
private RARRequestValidator validator;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() throws Exception {
validator = new RARRequestValidator();
authServer = new RARAuthorizationServer(validator);
tokenService = new RARTokenService();
objectMapper = new ObjectMapper();
}
@Test
void testPaymentAuthorizationRequest() {
// Create payment authorization detail
PaymentAuthorizationDetail paymentDetail = PaymentAuthorizationDetail.builder()
.type("payment")
.debtorAccount("DE89370400440532013000")
.creditorAccount("FR7630006000011234567890189")
.creditorName("Test Merchant")
.amount(new BigDecimal("99.99"))
.currency("EUR")
.build();
RichAuthorizationRequest request = RichAuthorizationRequest.builder()
.clientId("demo-client")
.responseType("code")
.redirectUri("https://client.example.com/callback")
.state("test-state")
.authorizationDetails(List.of(paymentDetail))
.build();
// Validate request
var errors = validator.validateJson(serializeRequest(request));
assertTrue(errors.isEmpty());
// Process request
AuthorizationResponse response = authServer.processAuthorizationRequest(request);
assertNotNull(response);
assertNotNull(response.getCode());
assertEquals("test-state", response.getState());
}
@Test
void testAccountInformationRequest() {
AccountInformationAuthorizationDetail accountDetail = AccountInformationAuthorizationDetail.builder()
.type("account_information")
.accountIds(List.of("DE89370400440532013000"))
.permissions(List.of(
AccountInformationAuthorizationDetail.AccountPermission.VIEW_BALANCE,
AccountInformationAuthorizationDetail.AccountPermission.VIEW_TRANSACTIONS
))
.includeBalance(true)
.build();
RichAuthorizationRequest request = RichAuthorizationRequest.builder()
.clientId("demo-client")
.responseType("code")
.authorizationDetails(List.of(accountDetail))
.build();
var errors = validator.validateJson(serializeRequest(request));
assertTrue(errors.isEmpty());
}
@Test
void testInvalidPaymentRequest() {
PaymentAuthorizationDetail invalidDetail = PaymentAuthorizationDetail.builder()
.type("payment")
.debtorAccount("invalid") // Invalid IBAN
.amount(new BigDecimal("-10.00")) // Negative amount
.build();
RichAuthorizationRequest request = RichAuthorizationRequest.builder()
.clientId("demo-client")
.responseType("code")
.authorizationDetails(List.of(invalidDetail))
.build();
var errors = validator.validateJson(serializeRequest(request));
assertFalse(errors.isEmpty());
}
@Test
void testConsentFlow() {
// Create request
PaymentAuthorizationDetail paymentDetail = PaymentAuthorizationDetail.builder()
.type("payment")
.debtorAccount("DE89370400440532013000")
.creditorAccount("FR7630006000011234567890189")
.amount(new BigDecimal("99.99"))
.currency("EUR")
.build();
RichAuthorizationRequest request = RichAuthorizationRequest.builder()
.clientId("demo-client")
.responseType("code")
.authorizationDetails(List.of(paymentDetail))
.state("test-state")
.build();
// Process initial request
AuthorizationResponse initResponse = authServer.processAuthorizationRequest(request);
String requestId = initResponse.getCode();
// Get consent details
var authRequest = authServer.getAuthorizationRequest(requestId);
assertNotNull(authRequest);
// Approve consent
Map<String, Boolean> consent = Map.of("0", true);
Map<String, Object> userInput = Map.of("note", "Approved via test");
AuthorizationResponse finalResponse = authServer.approveRequest(requestId, consent, userInput);
assertNotNull(finalResponse);
assertNotNull(finalResponse.getCode());
// Exchange code for token
RARAccessToken token = authServer.exchangeCodeForToken(
finalResponse.getCode(), "demo-client", "demo-secret"
);
assertNotNull(token);
assertNotNull(token.getAuthorizationDetails());
assertEquals(1, token.getAuthorizationDetails().size());
}
@Test
void testTokenCreationAndValidation() throws Exception {
// Create authorized details
AuthorizedDetail detail = AuthorizedDetail.builder()
.type("payment")
.authorizedData(Map.of(
"currency", "EUR",
"amount", 99.99
))
.grantedActions(List.of("CREATE", "READ"))
.build();
RARAccessToken token = RARAccessToken.builder()
.tokenValue("test-token")
.tokenType("Bearer")
.expiresIn(3600L)
.scope("payment")
.authorizationDetails(List.of(detail))
.additionalClaims(Map.of("client_id", "demo-client"))
.build();
// Create JWT
String jwt = tokenService.createRARToken(token);
assertNotNull(jwt);
// Parse and validate
RARAccessToken parsed = tokenService.parseAndValidateToken(jwt);
assertNotNull(parsed);
assertEquals("payment", parsed.getScope());
assertNotNull(parsed.getAuthorizationDetails());
}
@Test
void testMultiTypeAuthorization() {
PaymentAuthorizationDetail paymentDetail = PaymentAuthorizationDetail.builder()
.type("payment")
.debtorAccount("DE89370400440532013000")
.creditorAccount("FR7630006000011234567890189")
.amount(new BigDecimal("49.99"))
.currency("EUR")
.build();
AccountInformationAuthorizationDetail accountDetail = AccountInformationAuthorizationDetail.builder()
.type("account_information")
.accountIds(List.of("DE89370400440532013000"))
.permissions(List.of(
AccountInformationAuthorizationDetail.AccountPermission.VIEW_BALANCE
))
.build();
RichAuthorizationRequest request = RichAuthorizationRequest.builder()
.clientId("demo-client")
.responseType("code")
.authorizationDetails(List.of(paymentDetail, accountDetail))
.build();
var errors = validator.validateJson(serializeRequest(request));
assertTrue(errors.isEmpty());
}
private String serializeRequest(RichAuthorizationRequest request) {
try {
Map<String, Object> json = Map.of(
"client_id", request.getClientId(),
"response_type", request.getResponseType(),
"redirect_uri", request.getRedirectUri(),
"state", request.getState(),
"authorization_details", request.getAuthorizationDetails()
);
return objectMapper.writeValueAsString(json);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Configuration Example
application.yml
oauth:
rar:
enabled: true
supported-types: payment,account_information,document_access,location
max-details-per-request: 10
allow-custom-types: true
server:
issuer: https://auth.example.com
access-token-ttl: 3600
refresh-token-ttl: 86400
code-ttl: 300
clients:
- client-id: demo-client
client-secret: "{bcrypt}encoded-secret"
redirect-uris:
- https://client.example.com/callback
allowed-types: payment,account_information
jwk:
key-id: rar-signing-key
algorithm: RS256
key-size: 2048
Security Best Practices
1. Validate All RAR Parameters
public class RARSecurity {
public void validateRequest(RichAuthorizationRequest request) {
// Validate each authorization detail
for (AuthorizationDetail detail : request.getAuthorizationDetails()) {
validateType(detail.getType());
validateActions(detail.getActions());
validateLocations(detail.getLocations());
}
// Check for parameter injection
validateNoInjection(request.getAdditionalParameters());
}
}
2. Scope-to-RAR Mapping
public class RARScopeMapper {
public List<AuthorizationDetail> scopeToRAR(String scope) {
// Map traditional scopes to RAR details
// For backward compatibility
}
public String rarToScope(List<AuthorizationDetail> details) {
// Generate traditional scope string from RAR
}
}
3. Consent Gathering
public class RARConsentGatherer {
public ConsentScreen buildConsentScreen(AuthorizationRequest request) {
// Build granular consent UI based on RAR details
// Show each authorization type with specific parameters
// Allow user to approve/deny individual items
}
}
4. Audit Logging
@Aspect
@Component
public class RARAuditAspect {
@Around("@annotation(Audited)")
public Object auditRAR(ProceedingJoinPoint pjp) throws Throwable {
// Log all RAR requests and responses
// Include client ID, authorization details, consent decisions
// Store in secure audit log for compliance
}
}
Conclusion
Rich Authorization Requests (RAR) provide a powerful extension to OAuth 2.0 for fine-grained authorization:
Key Benefits
- Granular permissions beyond simple scopes
- Type-specific parameters for different resource types
- Structured authorization with JSON format
- Backward compatible with traditional OAuth
- Extensible for custom authorization types
Implementation Features
- Full RFC 9396 compliance
- Support for payment, account, document, and location types
- Comprehensive validation
- Token binding with embedded RAR details
- Resource server integration
- Client library for easy adoption
Use Cases
- Open Banking (PSD2) with payment and account access
- Healthcare with patient data access
- Document management systems
- Location-based services
- IoT device authorization
This implementation provides a production-ready RAR solution for OAuth 2.0 authorization servers, enabling fine-grained, structured authorization requests beyond traditional scopes.
Java Programming Basics – Variables, Loops, Methods, Classes, Files & Exception Handling (Related to Java Programming)
Variables and Data Types in Java:
This topic explains how variables store data in Java and how data types define the kind of values a variable can hold, such as numbers, characters, or text. Java includes primitive types like int, double, and boolean, which are essential for storing and managing data in programs. (GeeksforGeeks)
Read more: https://macronepal.com/blog/variables-and-data-types-in-java/
Basic Input and Output in Java:
This lesson covers how Java programs receive input from users and display output using tools like Scanner for input and System.out.println() for output. These operations allow interaction between the program and the user.
Read more: https://macronepal.com/blog/basic-input-output-in-java/
Arithmetic Operations in Java:
This guide explains mathematical operations such as addition, subtraction, multiplication, and division using operators like +, -, *, and /. These operations are used to perform calculations in Java programs.
Read more: https://macronepal.com/blog/arithmetic-operations-in-java/
If-Else Statement in Java:
The if-else statement allows programs to make decisions based on conditions. It helps control program flow by executing different blocks of code depending on whether a condition is true or false.
Read more: https://macronepal.com/blog/if-else-statement-in-java/
For Loop in Java:
A for loop is used to repeat a block of code a specific number of times. It is commonly used when the number of repetitions is known in advance.
Read more: https://macronepal.com/blog/for-loop-in-java/
Method Overloading in Java:
Method overloading allows multiple methods to have the same name but different parameters. It improves code readability and flexibility by allowing similar tasks to be handled using one method name.
Read more: https://macronepal.com/blog/method-overloading-in-java-a-complete-guide/
Basic Inheritance in Java:
Inheritance is an object-oriented concept that allows one class to inherit properties and methods from another class. It promotes code reuse and helps build hierarchical class structures.
Read more: https://macronepal.com/blog/basic-inheritance-in-java-a-complete-guide/
File Writing in Java:
This topic explains how to create and write data into files using Java. File writing is commonly used to store program data permanently.
Read more: https://macronepal.com/blog/file-writing-in-java-a-complete-guide/
File Reading in Java:
File reading allows Java programs to read stored data from files. It is useful for retrieving saved information and processing it inside applications.
Read more: https://macronepal.com/blog/file-reading-in-java-a-complete-guide/
Exception Handling in Java:
Exception handling helps manage runtime errors using tools like try, catch, and finally. It prevents programs from crashing and allows safe error handling.
Read more: https://macronepal.com/blog/exception-handling-in-java-a-complete-guide/
Constructors in Java:
Constructors are special methods used to initialize objects when they are created. They help assign initial values to object variables automatically.
Read more: https://macronepal.com/blog/constructors-in-java/
Classes and Objects in Java:
Classes are blueprints used to create objects, while objects are instances of classes. These concepts form the foundation of object-oriented programming in Java.
Read more: https://macronepal.com/blog/classes-and-object-in-java/
Methods in Java:
Methods are blocks of code that perform specific tasks. They help organize programs into smaller reusable sections and improve code readability.
Read more: https://macronepal.com/blog/methods-in-java/
Arrays in Java:
Arrays store multiple values of the same type in a single variable. They are useful for handling lists of data such as numbers or names.
Read more: https://macronepal.com/blog/arrays-in-java/
While Loop in Java:
A while loop repeats a block of code as long as a given condition remains true. It is useful when the number of repetitions is not known beforehand.
Read more: https://macronepal.com/blog/while-loop-in-java/