Documentatie afnemer (middels eigen implementatie)
Opbouwen redirect URL voor toestemmingsflow
De initiële redirect URL is van deze vorm:
GET https://<authorization_endpoint> parameters: response_type=code client_id=<client_id> redirect_uri=<redirect_uri> state=<state> scope=<scope> verify=<verify> code_challenge=<challenge> code_challenge_method=S256
Daarbij zijn de volgende verplichte parameters van toepassing:
authorization_endpoint |
De waarde van het |
client_id |
De Client ID die bij het aanmaken van de dienstengroep is getoond |
redirect_uri |
De callback URL waarnaar de OAuth server de browser van de energieconsument verwijst na afronding van het toestemmingsproces (al dan niet succesvol). Deze URL dient daarom publiek beschikbaar te zijn. |
state |
Een unieke referentie naar aanroep, die door de initiërende partij (d.w.z. de afnemer) wordt bepaald. Deze referentie wordt ook opgenomen in de callback URL, zodat kan worden bepaald om welke autorisatie-sessie het gaat. |
scope |
Verwijst naar de 'authorized scope' van een data product waar een consument toestemming voor gaat geven. Dit mag een lijst zijn indien er toestemming wordt gevraagd voor meerdere producten. Zie scopes voor meer informatie. |
verify |
Het huisnummer dat de energieconsument heeft opgegeven (zonder toevoeging). Dit wordt gebruikt om te verifiëren dat het huisadres waarvoor de energieconsument zich identificeert, overeenkomt met de gegevens die bij de afnemer zijn opgegeven. |
code_challenge |
Client side gegenereerde code challenge t.b.v. PKCE flow |
code_challenge_method |
Encryptie methode die is gebruikt om de |
Het toestemmingsplatform maakt gebruik van PKCE-beveiliging. Hiervoor dient eerst een code_verifier
gegenereerd te worden.
Een code_verifier
is een cryptografisch willekeurige tekenreeks met hoge entropie die gebruikmaakt van de niet-gereserveerde tekens [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", met een minimale lengte van 43 tekens en een maximale lengte van 128 tekens.
De code_verifier
moet voldoende entropie hebben om het onpraktisch te maken om de waarde te raden.
Er zijn twee methoden om op basis van de code_verifier
de code_challenge
te maken:
S256 (aanbevolen) |
De
|
plain |
De
|
De code_verifier
moet tezamen met de state
worden bewaard, zodat deze later kan worden gebruikt, bij het ophalen van een Access Token met een autorisatiecode.
Vind hier meer informatie over PKCE: https://oauth.net/2/pkce/
String clientId = "mijn_oauth_client";
String callbackURL = "https://...";
String sessionId = "..."; //random String; store this along with the codeVerifier for PKCE
String codeVerifier = "..."; //see documentation of PKCE flow above
String codeChallenge = "..."; //calculate this as
URI uri = UriComponentsBuilder.fromUriString(configurationResponse.authorizationEndpoint())
.queryParam("response_type", "code")
.queryParam("client_id", clientId)
.queryParam("redirect_uri", callbackURL)
.queryParam("state", sessionId)
.queryParam("scope", "target_dataproduct_scope")
.queryParam("verify", houseNumber)
.queryParam("code_challenge", codeChallenge)
.queryParam("code_challenge_method", "S256")
.build()
.toUri();
return ResponseEntity.status(HttpStatus.FOUND)
.location(uri)
.build();
Het programmatisch genereren van een geschikte code_verifier
en het berekenen van de bijbehorende code_challenge
wordt geïllustreerd door de onderstaande code; er zijn vele bibliotheken beschikbaar die dit volautomatisch doen.
package nl.edsn.common.afnemen.auth;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
public final class PkceUtil {
private static final String CODE_CHALLENGE_METHOD = "S256";
public String getCodeChallengeMethod() {
return CODE_CHALLENGE_METHOD;
}
public String generateCodeVerifier() {
SecureRandom secureRandom = new SecureRandom();
byte[] codeVerifier = new byte[32];
secureRandom.nextBytes(codeVerifier);
return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier);
}
public String generateCodeChallenge(String codeVerifier) {
try {
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(bytes, 0, bytes.length);
byte[] digest = messageDigest.digest();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Cannot generate code challenge", e);
}
}
}
Een eenmalig Access Token opvragen (JWT)
Het veld state
kan worden gebruikt om vast te stellen bij welke aanroep van de OAuth Authorization Code Grant Flow de callback hoort.Hier kan de opgeslagen code_verifier
mee worden opgehaald.
Nadat de code
via de callback URL is teruggekomen bij de applicatie, dient er een nieuwe aanroep naar het toestemmingsplatform te worden gedaan om deze autorisatiecode, uit de OAuth Authorization Grant Flow, om te zetten naar een OAuth Access Token.
Het OAuth Access Token wordt gebruikt om te authenticeren bij de data verstrekker.Ook bevat het Access Token een claim met de URL waarop de verstrekker bereikbaar is.
Om het Access Token te verkrijgen dient de volgende aanroep te worden gedaan:
POST https://<token_endpoint> Content-Type: application/x-www-form-urlencoded Data: grant_type=authorization_code code=<authorization_code> redirect_uri=<redirect_uri> code_verifier=<code_verifier> client_id=<clientId> client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer client_assertion=<client_assertion>
|
De waarde van het |
|
De authorization code die uit de OAuth Authorization Code Grant flow teruggegeven is via de callback URL. |
|
Dit is de Redirect URL die is opgegeven bij het registreren van de afnemende dienst. Deze URL is bij OAuth server bekend. Deze wordt ter verificatie gebruikt. dienst details |
|
De |
|
De Client ID die bij het aanmaken van de dienst is getoond |
|
Vaste waarde: urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
|
Getekende JWT zoals hier uitgelegd |
NB: Het gebruik van private_key_jwt authenticatie is hier optioneel, maar bij gebruik van confidential clients sterk aan te raden. Gebruik van PKCE is altijd verplicht.
Het antwoord op deze POST zal een JSON-object zijn die onder andere het attribuut access_token
bevat.
In geval van een niet succesvolle aanroep zal het een JSON object zijn met een error
en error_description
waarde.
Data ophalen met het Access Token
Het access token is een JWT token dat diverse claims bevat. Het is belangrijk dat de authenticiteit en integriteit van het token wordt gevalideerd. Dit kan met behulp van JWK (RFC7517; uitleg). Gebruik hiervoor de issuer
en de JWKS gegevens uit de jwks_uri
, beide uit de Oauth2 configuratie. Het is aan te raden een standaard library te gebruiken voor het valideren en verwerken van het JWT token.
Het access token ziet als volgt uit:
{
"sub": "3bf3f11b-7b1e-457b-9df2-80e53af24c21",
"aud": "jouw-client-id",
"nbf": 1731062306,
"scope": [
"scope-van-leverend-data-product"
],
"service_id": "jouw-client-id",
"iss": "https://www.mijnenergiedata.nl/autorisatieregister",
"resources": [
{
"endpoints": {
"single_sync": "https://dataproduct.host.com/dataproductA"
},
"scope": "scope-van-leverend-data-product"
}
],
"exp": 1731063206,
"iat": 1731062306,
"jti": "6cb96ae9-f13d-4aae-9371-b2c1e8d311bb",
"consent_id": "677c115c-7945-4533-baa6-3347e5632bc3",
"eans": [
"870751900000531268"
]
}
Als het token valide is dan is in de claim resources.<data_product_scope>.endpoints.single_sync
de URL te vinden die moet worden gebruikt om de gegevens bij de verstrekker op te halen.
Bij de aanroep om de gegevens op te halen moet het volledige token worden meegegeven in de Authorization
header met de prefix Bearer
:
Authorization: Bearer eyJhbGciO…
Als antwoord zal de data verstrekker de gevraagde data uit het verstrekkende systeem retourneren.
Indien er meerdere verstrekkende datadiensten zijn gekoppeld aan de afnemende datadienst, zal het token meerdere scopes met hun eigen endpoints bevatten. Deze endpoints moeten onafhankelijk van elkaar aangeroepen worden om de data per datadienst op te halen. Via configuratie van de afnemende dienst, is het mogelijk om voor een subset van de gekoppelde verstrekkende datadiensten toestemming te vragen en data op te vragen.
De response die terugkomt, wordt hier als simpele String
behandeld. In de praktijk zal deze conformeren aan het formaat zoals gespecificeerd door de verstrekkende datadienst. Neem contact op voor het exacte formaat.
Appendix A: ConsentAccessToken
en TokenMapper
TokenMapper
package nl.edsn.common.shared.security.token;
import nl.edsn.common.shared.security.token.converter.TokenConverter;
import org.slf4j.Logger;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Mapper that maps a data exchange access token into an internal representation.
*/
@Component
public class TokenMapper {
private static final Logger log = org.slf4j.LoggerFactory.getLogger(TokenMapper.class);
private final AuthorizationServerJwtDecoder jwtDecoder;
private final List<TokenConverter<? extends AccessToken>> accessTokenConverters;
public TokenMapper(AuthorizationServerJwtDecoder jwtDecoder, List<TokenConverter<? extends AccessToken>> accessTokenConverters) {
this.jwtDecoder = jwtDecoder;
this.accessTokenConverters = accessTokenConverters;
}
/**
* Maps the {@code token} into an {@link AccessToken} object.
*
* @param token the token as {@link String}.
* @return the mapped {@link AccessToken}.
* @throws TokenException when something went wrong mapping the {@code token}
*/
public <T extends AccessToken> T map(String token, Class<T> accessTokenClass) throws TokenException {
try {
final Jwt jwt = jwtDecoder.decode(token);
return map(jwt, accessTokenClass);
} catch (org.springframework.security.oauth2.jwt.JwtException | io.jsonwebtoken.JwtException ex) {
final String errorMessage = ex.getMessage();
log.warn("Jwt validation failed due to; {}", errorMessage);
throw new TokenException(errorMessage, ex);
}
}
@SuppressWarnings("unchecked")
public <T extends AccessToken> T map(Jwt jwt, Class<T> accessTokenClass) throws TokenException {
try {
log.debug("Parsing token");
return accessTokenConverters.stream()
.map(converter -> (TokenConverter<T>) converter)
.filter(converter -> converter.supports(accessTokenClass))
.filter(converter -> hasRequiredClaims(jwt, converter))
.map(converter -> converter.convert(jwt))
.findFirst()
.orElseThrow(() -> new TokenException("Token invalid", null));
} catch (org.springframework.security.oauth2.jwt.JwtException | io.jsonwebtoken.JwtException ex) {
log.warn("Failed parsing token", ex);
throw new TokenException("Token invalid", ex);
}
}
private static boolean hasRequiredClaims(Jwt jwt, TokenConverter<? extends AccessToken> converter) {
final Set<String> requiredClaimNames = converter.requiredClaimNames();
final List<String> missingClaims = requiredClaimNames.stream()
.filter(claim -> !jwt.hasClaim(claim))
.collect(Collectors.toList());
if (!missingClaims.isEmpty()) {
log.warn("Not all claims for converter '{}' are found in jwt, missing {}", converter.getClass(), missingClaims);
}
return missingClaims.isEmpty();
}
}
ConsentAccessToken
package nl.edsn.common.shared.security.token;
import java.time.Instant;
import java.util.List;
public class ConsentAccessToken extends AccessToken {
private String consentId;
private List<String> eans;
public ConsentAccessToken(final String consumingServiceId,
final String consentId,
final List<AccessTokenResource> resources,
final List<String> eans,
final List<String> scope,
final Instant expiresAt) {
super(consumingServiceId, resources, scope, expiresAt);
this.consentId = consentId;
this.eans = eans;
}
public ConsentAccessToken() {
}
public String getConsentId() {
return this.consentId;
}
public List<String> getEans() {
return this.eans;
}
public void setConsentId(String consentId) {
this.consentId = consentId;
}
public void setEans(List<String> eans) {
this.eans = eans;
}
public String toString() {
return "ConsentAccessToken(consentId=" + this.getConsentId() + ", eans=" + this.getEans() + ")";
}
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ConsentAccessToken)) {
return false;
}
final ConsentAccessToken other = (ConsentAccessToken) o;
if (!other.canEqual((Object) this)) {
return false;
}
if (!super.equals(o)) {
return false;
}
final Object this$consentId = this.getConsentId();
final Object other$consentId = other.getConsentId();
if (this$consentId == null ? other$consentId != null : !this$consentId.equals(other$consentId)) {
return false;
}
final Object this$eans = this.getEans();
final Object other$eans = other.getEans();
if (this$eans == null ? other$eans != null : !this$eans.equals(other$eans)) {
return false;
}
return true;
}
protected boolean canEqual(final Object other) {
return other instanceof ConsentAccessToken;
}
public int hashCode() {
final int PRIME = 59;
int result = super.hashCode();
final Object $consentId = this.getConsentId();
result = result * PRIME + ($consentId == null ? 43 : $consentId.hashCode());
final Object $eans = this.getEans();
result = result * PRIME + ($eans == null ? 43 : $eans.hashCode());
return result;
}
}
Appendix B: OpenAPI Specificaties
OpenAPI specificatie voor de callback URL, te implementeren door de afnemende dienst: afnemer.yaml
OpenAPI specificatie voor de aan te roepen dataverstrekker (uit de endpoints.single_sync
claim van de resource): generiek-verstrekken-single.yaml
Appendix C: Code voorbeelden
Voorbeeldcode o.a. voor het opbouwen van de url om toestemming te vragen en het maken van een client-assertion.