Terug

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 authorization_endpoint attribuut uit de OAuth configuratie

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 code_verifier t.b.v. PKCE flow om te zetten in code_challenge.

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 code_challenge is de Base64URL (zonder opvulling) gecodeerde SHA256-hash van de code_verifier.

code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

plain

De code_challenge is gelijk aan de code_verifier.

code_challenge = code_verifier

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/

Een voorbeeld in code voor het opbouwen van een redirect naar het toestemmingsplatform voor het vragen van toestemming, o.b.v. Java en Spring Web, met de waardes uit de eerder aangemaakte dienst:
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>

token_endpoint

De waarde van het token_endpoint attribuut uit de OAuth2 configuratie

code

De authorization code die uit de OAuth Authorization Code Grant flow teruggegeven is via de callback URL.

redirect_uri

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

code_verifier

De code_verifier waaruit de code_challenge was gegenereerd die bij de initiële redirect was meegegeven.

client_id

De Client ID die bij het aanmaken van de dienst is getoond

client_assertion_type

Vaste waarde: urn:ietf:params:oauth:client-assertion-type:jwt-bearer

client_assertion

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.