Skip to content

Instantly share code, notes, and snippets.

@georgepadayatti
Last active May 2, 2023 17:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save georgepadayatti/d534dd267674c99fbdf5e61565eb9f56 to your computer and use it in GitHub Desktop.
Save georgepadayatti/d534dd267674c99fbdf5e61565eb9f56 to your computer and use it in GitHub Desktop.
Integrate iGrant.io Data Pod to an application

1.0 Authentication

1.1 Obtain the DataPod URL from the Individual

An application can ask the user to the enter the URL of their datapod. for e.g. this would look like https://george.datapod.igrant.io

If the individual provides a URL that doesn't end with /profile/card#me path then, append that to the provided URL.

1.2 Obtain the OIDC issuer URL for DataPod

1.2.1 Fetch the WebID document

Append /profile/card#me to the pod URL and perform HTTP GET to obtain the WebID document.

curl -X GET https://george.datapod.igrant.io/profile/card\#me

1.2.2 Get the OIDC issuer URL

Apply the regex pattern to get the solid OIDC URL from the response text.

solid:oidcIssuer\s+(<[^>]+>)

The above regex captures the OIDC URL and can be accessed from the captured group. Apply the same using the techniques available for the implementation language.

1.3 Obtain the well-known OpenID configuration

Append /.well-known/openid-configuration to the OIDC issuer URL and perform HTTP GET to obtain the OpenID configuration in JSON format. A sample configuration is as below:

{
  "issuer": "https://datapod.igrant.io",
  "jwks_uri": "https://datapod.igrant.io/jwks",
  "scopes_supported": [
    "openid",
    "offline_access"
  ],
  "response_types_supported": [
    "code",
    "code token",
    "code id_token",
    "id_token code",
    "id_token",
    "id_token token",
    "code id_token token",
    "none"
  ],
  "token_types_supported": [
    "legacyPop",
    "dpop"
  ],
  "response_modes_supported": [
    "query",
    "fragment"
  ],
  "grant_types_supported": [
    "authorization_code",
    "implicit",
    "refresh_token",
    "client_credentials"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic"
  ],
  "token_endpoint_auth_signing_alg_values_supported": [
    "RS256"
  ],
  "display_values_supported": [],
  "claim_types_supported": [
    "normal"
  ],
  "claims_supported": [],
  "claims_parameter_supported": false,
  "request_parameter_supported": true,
  "request_uri_parameter_supported": false,
  "require_request_uri_registration": false,
  "check_session_iframe": "https://datapod.igrant.io/session",
  "end_session_endpoint": "https://datapod.igrant.io/logout",
  "authorization_endpoint": "https://datapod.igrant.io/authorize",
  "token_endpoint": "https://datapod.igrant.io/token",
  "userinfo_endpoint": "https://datapod.igrant.io/userinfo",
  "registration_endpoint": "https://datapod.igrant.io/register"
}

Store authorization_endpoint and token_endpoint from OpenID configuration for later use.

1.4 Dynamic OpenID client registration

Append /register to the OIDC issuer URL obtained earlier and HTTP POST with below request body to perform dynamic OpenID client registration.

{
  "client_name": "<client_name>",
  "application_type": "web",
  "redirect_uris": "<array of redirect_uris>",
  "subject_type": "public",
  "token_endpoint_auth_method": "client_secret_basic",
  "id_token_signed_response_alg": "RS256",
  "grant_types": ['authorization_code', 'refresh_token']
}
  • client_name: Can be random name. For e.g. Data Wallet
  • redirect_uris: Must be an array of URLs. Upon successful login, the individual would be redirected to this URL. For Android application, this is preferred to be a dynamic link and for iOS this could be universal link. Upon redirection, this URL would also receive a query parameter - code. This code must be then exchanged to obtain the access_token and refresh_token pairs.

Save the client registration response JSON which would contain the client_id required in the later stages.

Note: One possible optimisation that can be done in the application side would be to check the OIDC issuer URL, and don't register the client application more than once for the same URL.

1.5 Perform authorization

1.5.1 Proof Key for Code Exchange (PKCE)

  1. Generate a random 64 byte string. This is called as code verifier. This value need to be stored in the local storage as it is required to obtain the access_token and refresh_token in the later steps.
  2. Now let's prepare code challenge using the code verifier.
    1. Encode the code verifier as ASCII.
    2. Take SHA256 digest of the code verifier.
    3. Base64 URL safe encode the digest and decode the it as ASCII.
    4. Strip the '=' characters at the end of the encoded string and this string becomes the code challenge.

1.5.2 Prepare the authorization URL

Append the following query params to the authorization_endpoint that was obtained in Step 1.3.

  1. response_type - The default value is code
  2. redirect_uri - URL same as the redirect_uris in Step 1.4, but as a string and not array.
  3. scope - The default value is openid webid offline_access
  4. client_id - The client ID obtained in Step 1.4
  5. code_challenge_method - The default value is S256
  6. code_challenge - The code challenge created in Step 1.5.1

This authorization URL will take the Individual to Data Pod login page. For e.g. In Data Wallet app, a webview will open up and navigate the Individual to prepared authorization URL with query params.

On successful login, the login page will be redirected to the redirect_uri configured.

1.6 Handle redirect

Fetch the authorization code from the code query param in the redirect_uri.

1.6.1 Generate DPoP JWT header

  1. Generate a random uuid4 string. This is DPoP unique identifier.
  2. Generate elliptic curve P256 private key, public key pair with random key id (Random uuid4 string).
  3. Export the public key JWK. A sample is given below:
{
  "kty": "EC",
  "use": "sig",
  "alg": "EC",
  "kid": "abc",
  "crv": "P-256",
  "x": "avZfzz-fM7uL85jpxfUfYFBpWnX3RY_mt94bHL7RF9o",
  "y": "llhm4mT2b7R9Enki8cei1DIcqMt1Y6XzXnMo0eWNy0A"
}
  1. Create a JWT token and sign it using the keypair created in the previous steps.

JWT header should be as given below:

{
  "alg": "ES256",
  "typ": "dpop+jwt",
  "jwk": "<public key JWK>"
}

JWT claims should be as given below:

{
  "htu": "<http_token_usage_url in this it is token_endpoint saved in Step 1.3>",
  "htm": "<http_method in this case it is POST>",
  "jti": "<dpop_unique_identifier>",
  "iat": "epoch time in seconds"
}

Note: The public key and private key pair must be saved for further use.

1.6.2 Exchange authorization code to obtain access token

Perform HTTP POST against the token endpoint. With following headers and request body:

Headers:

{
  "DPoP": "<dpop_jwt from previous step>",
  "content-type": "application/x-www-form-urlencoded"
}

Request body:

{
  "grant_type": "authorization_code",
  "code_verifier": "<code_verifier>",
  "code": "<authorization_code>",
  "client_id": "<client_id>",
  "redirect_uri": "<redirect_uri>"
}

The response shall contain the access_token and refresh_token. This needs to persisted in local storage should be not be lost when Data Wallet app reopens.

Note: The access token should also be refreshed if it expires.

In order to refresh an access token, the request body should be:

{
  "grant_type": "refresh_token",
  "refresh_token": "<refresh_token>",
  "client_secret": "<client_secret obtained in Step 1.4>",
  "client_id": "<client_id>"
}

2.0 CRUD on resources/containers

2.1 Create a folder

2.1.1 Generate DPoP JWT header

  1. Generate a random uuid4 string. This is DPoP unique identifier.
  2. Create a JWT token and sign it using the keypair created in the Step 1.6.1

JWT header should be as given below:

{
  "alg": "ES256",
  "typ": "dpop+jwt",
  "jwk": "<public key JWK>"
}

JWT claims should be as given below:

{
  "htu": "<http_token_usage_url>",
  "htm": "PUT",
  "jti": "<dpop_unique_identifier>",
  "iat": "epoch time in seconds"
}

http_token_usage_url is datapod_url + new folder path for .e.g. https://george.datapod.igrant.io/DataWallet/Backups/

Remember to put slash character (/) at the very end of the new folder path.

2.1.2 Perform HTTP PUT

Perform HTTP PUT operation against http_token_usage_url in the previous step, with following headers:

{
  "accept": "text/turtle;q=1.0, */*;q=0.5",
  "authorization": "DPoP <access_token>",
  "dpop": "<dpop_jwt>",
  "link": "<http://www.w3.org/ns/ldp#BasicContainer>; rel='type'"
}

2.2 Upload a file

2.2.1 Generate DPoP JWT header

  1. Generate a random uuid4 string. This is DPoP unique identifier.
  2. Create a JWT token and sign it using the keypair created in the Step 1.6.1

JWT header should be as given below:

{
  "alg": "ES256",
  "typ": "dpop+jwt",
  "jwk": "<public key JWK>"
}

JWT claims should be as given below:

{
  "htu": "<http_token_usage_url>",
  "htm": "POST",
  "jti": "<dpop_unique_identifier>",
  "iat": "epoch time in seconds"
}

http_token_usage_url is datapod_url + folder path to where this file uploaded for .e.g. https://george.datapod.igrant.io/DataWallet/Backups/

Remember to put slash character (/) at the very end of the folder path.

2.2.2 Perform HTTP POST

Perform HTTP POST operation against http_token_usage_url in the previous step, with following headers:

{
  "accept": "*/*",
  "authorization": "DPoP <access_token>",
  "dpop": "<dpop_jwt>",
  "slug": "<file name>",
  "content-type": "<mime_type_of_file for e.g. application/zip for zip file>"
}

The request should also contain the file content bytes to be uploaded. This depends on the implementation language and http client library how you can post a file to the server.

2.3 Download a file

2.3.1 Generate DPoP JWT header

  1. Generate a random uuid4 string. This is DPoP unique identifier.
  2. Create a JWT token and sign it using the keypair created in the Step 1.6.1

JWT header should be as given below:

{
  "alg": "ES256",
  "typ": "dpop+jwt",
  "jwk": "<public key JWK>"
}

JWT claims should be as given below:

{
  "htu": "<http_token_usage_url>",
  "htm": "GET",
  "jti": "<dpop_unique_identifier>",
  "iat": "epoch time in seconds"
}

http_token_usage_url is datapod_url + file path for .e.g. https://george.datapod.igrant.io/DataWallet/Backups/backup.zip

Remember to put slash character (/) at the very end of the folder path.

2.3.2 Perform HTTP GET

Perform HTTP GET operation against http_token_usage_url in the previous step, with following headers:

{
  "accept": "text/turtle;q=1.0, */*;q=0.5",
  "authorization": "DPoP <access_token>",
  "dpop": "<dpop_jwt>"
}

The request will download the file to the local file system. This depends on the implementation language and http client library how you can download a file to local file system.

2.4 List files in a folder

2.4.1 Generate DPoP JWT header

  1. Generate a random uuid4 string. This is DPoP unique identifier.
  2. Create a JWT token and sign it using the keypair created in the Step 1.6.1

JWT header should be as given below:

{
  "alg": "ES256",
  "typ": "dpop+jwt",
  "jwk": "<public key JWK>"
}

JWT claims should be as given below:

{
  "htu": "<http_token_usage_url>",
  "htm": "GET",
  "jti": "<dpop_unique_identifier>",
  "iat": "epoch time in seconds"
}

http_token_usage_url is datapod_url + file path for .e.g. https://george.datapod.igrant.io/DataWallet/Backups/

Remember to put slash character (/) at the very end of the folder path.

2.4.2 Perform HTTP GET

Perform HTTP GET operation against http_token_usage_url in the previous step, with following headers:

{
  "accept": "text/turtle;q=1.0, */*;q=0.5",
  "authorization": "DPoP <access_token>",
  "dpop": "<dpop_jwt>"
}

Apply the regex pattern to get all the resources in the folder

ldp:contains\s+(.*)[;\.]

The above regex captures the resources list and can be accessed from the captured group. Apply the same using the techniques available for the implementation language.

Iterate through the list and remove prefix, suffix angle brackets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment