Introduction

This post will go over my attempts at trying to recreate the Solid Project's first app demostration, but instead of using Javascript or Solid's client libraries I'll be using C# and Blazor WebServer to build the client end. I'll be walking the Solid OIDC Primer along the way.

As I've been learning, I've decided to make the examples available on my GitHub, licensed as MIT as "solidproject-dotnet-examples." I plan on making two branches, "main" for examples that are "finished" and ready for consumption, and "dev" for myself as I'm learning and not cleaned up.

This post specifically will cover the example project named "Auth_Example."

In the "first app" example from Solid, we login to the Community Solid Server and can get and write our first name to our WebID. We'll attempt to do the same thing with our "Auth_Example" application.

In This Post

ItemVersionRemarksLinks
Community Solid Server2.0.1 Link
Auth_Example.NET 6Blazor Server ApplicationLink
IdentityModel.OIDCClient5.0.0Used in "Auth_Example"Link
DotNetRDF2.7.2Used in "Auth_Example"Link
Visual Studio Community 2022 Used in development 

Assumptions

This post makes the following assumptions:

  • Community Solid Server will be running at http://localhost:3000
  • Auth_Example will be running at https://localhost:7030/

Setup

Community Solid Server

First, install Community Solid Server and start it using in-memory storage by running from a command line:

community-solid-server

After starting, navigate to http://localhost:3000 and run the setup. You'll want to choose "Sign me up for an account." For this demo, we will choose "Create a new WebID for my Pod" and "Create a new Pod with my WebID as the owner ... in the root."

You will be prompted to create an account with an email address and a password, for demo purposes I simply used "test@test.com" and "1234."

If everything is successful, you'll be greeted with a "Server Setup Complete" message explaining that your new WebId is located at "http://localhost:3000/profile/card#me."

Auth_Example

You can clone the repro "solidproject-dotnet-examples" locally and open up the solution file in Visual Studio "solidproject-dotnet-examples/auth/auth_example/auth_example.sln."

This should startup the project, which is just the default Blazor Server template with a few modified components.

Login

The login page has an explanation of what will happen when you click on "Login To Community Solid Server."

When you click "Login To Community Solid Server" the function

LoginToSolid()

is called. At a high level, the steps the page takes are:

Register the app using Dynamic Client Registration. If you're leveraging the Solid OIDC Primer as a guide, we are at Step 6's footnote of "Authorization Request." This gives us a CLIENT_ID and CLIENT_SECRET that we'll need to hang on to, so we save these in storage.

Generate a login url and redirect the user to the login of the Community Solid Server. Since Solid leverages OpenID, we're able to leverage the OIDCClient class from the IdentityModel library to handle the CODE_CHALLENGE and CODE_VERIFIER steps for us. We'll save the CODE_VERIFIER to browser storage because we'll need it in the next step.

Handle the redirect back from the Community Solid Server. Once the user has logged into Community Solid Server, it will redirect back to our page along with an APP_CODE that we need to save in the query string. This is step 10 and 11 of the OIDC primer under the "Authorization Code PKCE Flow" section.

To handle the redirect back, we'll leverage Blazor's

OnInitialized()

function to parse the query string values as well as the

LocationChanged()

event of Navigation Manager. Finally, we'll also leverage Blazor's

OnAfterRenderAsync()

to request Id and Access tokens from the Community Solid Server.

Note that we're saving the APP_CODE value, parsed from the redirect back in the query string using the function

GetQueryStringValues()

Get Id and Access Tokens. Once the above functions have ran after the redirect to us, we run the function

GetAccessAndIdTokens()

to handle getting tokens that we can then later use to update our first name on our WebID. This is done with an HTTP POST request that we'll again leverage the IdentityModel.OIDCClient library for.

This starts steps 12-14 of the OIDC primer section "Authorization Code PKCE Flow" mentioned earlier.

Generating a Json Web Token (JWT)

To start this process, we need to generate a Json Web Token (JWT) that will be sent in the HTTP Header as the "DPoP" (Demonstration of Proof of Possesion) value. Our goal is to produce a JWT that looks like the value in Step 13 of PKCE section of the Primer. Something like (adapted from that page):

Header:

{
"alg": "RS256",
"typ": "dpop+jwt",
"jwk": {
"e": "AQAB",
"kty": "RSA",
"n": "wwVr2WTCVB2ivM2-6fXsI3L4I1elJkkipoD8vvuuF7Hfq6CSKYpct-K8m7Q-_PaMEt4qq0qZRzGbPKy3OkuxTnE1hbDg1N-W5IF4uZuAzVtXrnisvXsEHS2CRr3cG8ZR_IijJjbq60_gYxTN3FR8n7gIoNe8oyO2GZSEvlapSrSDhOgblFFsezSbVl1MZPftDYo_R5s0bLlYRh8T0OPVj5LMBM_fD9o5tzQL1guWQxEPbFrQI-pde0ocpOzPX1Hgy12j0pkDiyAvEEURsKMda_kQTClG1buYmSTfnj-vlypWkavZiLA43gn4zlNXfTAJq61uHz2uM20aj8wvNoSlgQ"
}
}

Where items are:

ItemDescription
algThe algorithm, in this case, RSA256
typThe type of token, in this case "dpop+jwt"
jwkThe key of our token, in this case, a public RSA key

Body:

{
"exp": 1643037740,
"iss": "https://localhost:7030/login",
"aud": "https://localhost:7030/login",
"htu": "http://localhost:3000/idp/token",
"htm": "POST",
"jti": "29d7c642-954d-47f4-a120-9e056fc3d599",
"iat": 1642951340
}

Where the claims are:

ItemDescription
expExpiration time of the token
issThe issuer of the token, in this case, ourselves
audThe audience of the token, in this case, ourselves
htuWho is using this token, in this case, the Community Solid Server
htmThe method we're using this for, in this case, an HTTP POST
jtiA unique id for the token, in this case, one we generated dynamically
iatThe time the token was issued, presented as an integer value

For our JWT, we'll need to generate keys to sign our token. In the "Auth_Example" app, we leverage RSA to generate a public/private pair in the function "GenerateKeys()." We'll then go about constructing a JWT with the above items, putting our public key in the header and we'll sign the key with our private key.

I won't go through all the details of generating the JWT, but the code should be self explanatory and I've tried to add comments along the way. We leverage Microsoft's

System.IdentityModel.Tokens.Jwt 

namespace to actually generate our token.

Requesting Tokens

Once we have a token, we'll generate an HTTP POST to the Community Solid Server to try and get our tokens. This is Step 14 of the OIDC Primer's Authorization Code PKCE Flow section.

The post looks something like (taken from the primer):


POST https://localhost:3000/idp/token
Headers: {
  "DPoP": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwia2lkIjoiZkJ1STExTkdGbTQ4Vlp6RzNGMjVDOVJmMXYtaGdEakVnV2pEQ1BrdV9pVSIsInVzZSI6InNpZyIsImFsZyI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiOWxlT2gxeF9IWkhzVkNScDcyQzVpR01jek1nUnpDUFBjNjBoWldfSFlLMCIsInkiOiJqOVVYcnRjUzRLVzBIYmVteW1vRWlMXzZ1cko0TFFHZXJQZXVNaFNEaV80In19.eyJodHUiOiJodHRwczovL3NlY3VyZWF1dGguZXhhbXBsZS90b2tlbiIsImh0bSI6InBvc3QiLCJqdGkiOiI0YmEzZTllZi1lOThkLTQ2NDQtOTg3OC03MTYwZmE3ZDNlYjgiLCJpYXQiOjE2MDMzMDYxMjgsImV4cCI6MTYwMzMwOTcyOH0.2lbgLoRCkj0MsDc9BpquoaYuq0-XwRf_URdXru2JKrVzaWUqQfyKRK76_sQ0aJyVwavM3pPswLlHq2r9032O7Q",
  "content-type": "application/x-www-form-urlencoded"
}
Body:
  grant_type=authorization_code&
  code_verifier=JXPOuToEB7&
  code=m-OrTPHdRsm8W_e9P0J2Bt&
  redirect_uri=https%3A%2F%2Fdecentphotos.example%2Fcallback&
  client_id=https%3A%2F%2Fdecentphotos.example%2Fwebid%23this

But instead of us generating this HTTP POST manually we'll leverage again the IdentityModel.OIDCClient library to do this for us.

First, as mentioned, we'll add the "DPoP" token we just constructed to the header:

client.DefaultRequestHeaders.Add("DPoP", jwtToken);

And then we'll leverage the IdentityModels' extension method:


  var response = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
            {
                Address = identityProvider + "/idp/token",

                ClientId = clientId,
                ClientSecret = clientSecret,

                Code = appCode,
                RedirectUri = navigation.BaseUri + "login",

                // optional PKCE parameter
                CodeVerifier = codeVerifier,

                GrantType = "authorization_code",

                ClientCredentialStyle = ClientCredentialStyle.PostBody
            });

Where:

ItemDescription
AddressWhere the token endpoint is, in this case, usually http://localhost:3000/idp/token. Note that you can discover this by examining http://localhost:3000/.well-known/openid-configuration (or for any OpenId server, ".well-known/openid-configuration")
ClientIdThe CLIENT_ID value we saved from earlier
ClientSecretThe CLIENT_SECRET value we saved from earlier
CodeThe APP_CODE value we saved from earlier from the redirected query string
RedirectUriWhere we want to be redirected to, in this case, our login page
GrantTypeauthorization_code
ClientCredentialStyleThis is stating that these values will be posted in the Body of the HTTP request, rather than in the header

If all goes correctly, the Community Solid Server should send back to us two JWT, one as an ID_TOKEN and one as an ACCESS_TOKEN. We'll need the access token to do stuff with our WebID, but we'll save both of them to storage for now.

This completes the login flow.

Login With Client

There is a second page in the "Auth_Example" solution that essentially does everything above, but tries to move most of the code away from the page into the class "SolidDotNetClient.cs."

This is the page that tries to mimic the "first app" demo from the Solid Project website.

On this page you should find three buttons: one to login, one to get your WebID, and one to write a new name to your WebID.

Clicking the "Login To Solid Community Server" button essentially does everything described earlier in the Login section of this post.

Getting your First Name From Community Solid Server

Clicking the "Get Card Information" will run the function

GetCardWithRdf()

which leverages the DotNetRDF library to get the current saved First Name value.


    ///
    /// Parses the Card Uri with the dotNetRDF library to get the first name from the card.
    /// 
    private void GetCardWithRdf()
    {
        IGraph g = new Graph();
        UriLoader.Load(g, new Uri(cardUrl));

        var triples = g.Triples;
        foreach (var triple in triples)
        {
            if (triple.Predicate.NodeType == NodeType.Uri)
            {
                var uriNode = triple.Predicate as UriNode;
                if (uriNode.Uri.Fragment.Contains("#fn"))
                {
                    if (triple.Object.NodeType == NodeType.Literal)
                    {
                        var literal = triple.Object as ILiteralNode;
                        currentName = literal.Value;
                        oldName = currentName;
                    }
                }
            }
        }
    }

This sets the string variables "currentName" and "oldName" on the page, which "currentName" is bound to the input box on the page.

Updating your First Name at Community Solid Server

To update your name, we call the function

SetNameAsync()

Which executes an HTTP PATCH against Community Solid Server using SPARQL to update your name. The goal is to produce an HTTP PATCH that looks similar to:


PATCH http://localhost:3000/profile/card HTTP/1.1
Host: localhost:3000
Authorization: DPoP eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktNd3lyWGxRdzVpZ1B3YzVyQ1p1YWQ2YUNaVEtpZ0Z1VGlOamtFc3E5WGsifQ.eyJ3ZWJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9wcm9maWxlL2NhcmQjbWUiLCJjbGllbnRfaWQiOiJCZXFQcTZHeHlERjE3dkw5cUVjcVkiLCJqdGkiOiJuRGNYMUZTYVhRMGM3THI5R1diRW8iLCJzdWIiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvcHJvZmlsZS9jYXJkI21lIiwiaWF0IjoxNjQyODA5MzkxLCJleHAiOjE2NDI4MTI5OTEsInNjb3BlIjoib3BlbmlkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwLyIsImF1ZCI6InNvbGlkIiwiYXpwIjoiQmVxUHE2R3h5REYxN3ZMOXFFY3FZIiwiY25mIjp7ImprdCI6Ik9mMTNLOEVFemIzRkNQUFpwMXhCTTM3NnJKRHZDSEVkeEhwZVR3Z1VFdkkifX0.1aMSm7Dp3K_563ce9vwfz07qYL8BPajymDWoewj1uglj9mSUKSlrejUqFSN9eYwl9KoqyB-c_MuZJLLvDJy4qDLZmfLPx8OSkbGP5aPLCpxjRfzus0TEOZxtw7cfVYPQZ38VKlRDCbOUfOcMfdD3QtNMSfDnyWdNSThVXO7lvl7djkvEKGEYqdUKM5AGbvoKj-S7hgLtYHl8z76TiUUX2TxDSwxjqQ8pQC0Y_AoeCpO_5NGx4BXw_B2cl0b3BT4sKc8e4fA-VnlgtlAVNY_O4d25NW4Jjhn4XMWwQfl6gE8pGc7xHU53eRcK91zYMMRTfF5AZG55_R0hB9U8JBBe1w
dpop: eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiJ1d0RUSFhzcXNhemZibWVFVzFrck9Bdk1veWZONUNNU21sSjdNSGRMMWg5b2hpQnF5OWdITEVTVXhrcTZWNVE4SDRpS3ZHejJIZGNPRTlndmtFa2E3WUtlNFo0eXZhZWVvVm5ZMWktUUpyWnBSMFhrc096ckRSYUpabGF6WXNNVHNKbnZUeW1kX2J3M3E0R0dBbS1ydzA5VWpkQzFXUnM5czZRUk90MWRrR01RUkpfQ3Y4blQ4WE9wUDZaOXVkZEpsaVd6TFJvTk9Oekg5TGRmQXg1QkpFLWhZNmxLU2FpVG9hWkVReW1GNkpIc2ZsYkZZSlpiTG50ZURUQkxNcWNqLTBkYU1KRlJieTFYcmMxekFrejFBdVl0bjhJaF9vSnFXaXhjSFZEaDNvejhmOXhkZ2Y0ZFd2d0R4emNjRi0xU0kyTjlBb0JLQXVycVZlY21KekpoRVEifSwidHlwIjoiZHBvcCtqd3QifQ.eyJodHUiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvcHJvZmlsZS9jYXJkIiwiaHRtIjoiUEFUQ0giLCJqdGkiOiIyZTVlOThjZC0wYjQ4LTRkODAtYTVmZS0wMjgzZGJkMzU3YzQiLCJpYXQiOjE2NDI4MDkzOTF9.Ucf3ys-3pjj3aPVO_LfBXddhVpvoNQpc6cQ-ExV4LvR4a7OLzSHD3f04cpAd7Gj1aXW-HaOpDEsDb2lvev4ck8hNCUPxsWFPtVxpqbV6Oj0TMO0sd_74fHKqBwKnkKZXd-nNOSSgCicV-_URfha4WMyxfj8Q2XW_uyseUIOzIuTb3ifwFZrNDH2TOecoAkqVkaS5Y1BPDe2xlZBzis8BNT1iMOKfEypNw39qRrOG9dEEU_ZKoHPJHEvumeivHrZz56ddwPaiZkT-Y-ZVszy8klRpgEX73clFWhMnMYZlnCgOvomV9lB9jt-dqKbL9cT9yAmB4OKFp8YBm9XkxSMk4g
Referer: https://localhost:7030/loginclient
Origin: https://localhost:7030/loginclient
traceparent: 00-bde72432b7deb87c38fbb7f18a84c52d-2854212c7bf7fae7-00
Content-Type: application/sparql-update
Content-Length: 196

DELETE DATA {<http://localhost:3000/profile/card#me> <http://www.w3.org/2006/vcard/ns#fn> "".}; INSERT DATA {<http://localhost:3000/profile/card#me> <http://www.w3.org/2006/vcard/ns#fn> "My Name".}; 

Where:

ItemDescription
Authorization"DPoP" + our ACCESS_TOKEN after we logged in from earlier
dpopA new generated JWT specifically for this action of updating our first name
Content Body (SPARQL)A DELETE and INSERT SPARQL statements to update our name, sent in the content body of the HTTP PATCH

Note: the other headers I believe are not required.

Generating a JWT for PATCH action

As you can see from above, we need to generate a dpop token, this time for our action of using HTTP PATCH for the WebID URL. We do this with the line

client.BuildJwtForContent("PATCH", cardUrlNoAnchor);

Which builds us a JWT like:

Header:


{
  "alg": "RS256",
  "jwk": {
    "e": "AQAB",
    "kty": "RSA",
    "n": "6i9G5I2mJWkaHjsgVmMQTG_3RcEd3TQOcNBfpip_t29M8D1bgarIqjQlhXBqrw27rocMH9Bq6YikGizAsvjMKNo5D7K2HMip2jKX5iV8mQCsDsqnSigTGmSQDZyMRmU6HC86BCluea-DRcRkjrpihX1qG6TSb5V1_acqTfTYmgtKh9ZpFM8oFsJAxxKkcHYa9UhG6t7bMLkvZ2yaV-HQjcgYHWSmSJb4P4MOqk1HoSXWSz3VFBmDYBOxkRtbOId3xM4_3xgoourJBPhwYgRgosW9KaB_iVISsgG3BBumdhxMSJV1MylRO462SMCPtH979m08NGW2sm4KNAV9ZKhV2Q",
    "alg": "RS256"
  },
  "typ": "dpop+jwt"
}

Body:


{
  "htu": "http://localhost:3000/profile/card",
  "htm": "PATCH",
  "jti": "e625bb39-78ff-45b7-a7e5-5c11b54da856",
  "iat": 1642954923
}

For the header, this is just an identifier of the algorithm, in this case, RS256, and our public web key from earlier. Finally we identify the type by "dpop+jwt".

Note: I ran into a problem where Community Solid Server would not work unless I included in the JWK (Json Web Key) the "alg" attribute within the key itself.

For the body, we are stating that we wish to modify our WebId card at "http://localhost:3000/profile/card" and we are doing so via the HTTP PATCH method.

We again sign the key using our private key.

Finally, we actually modify our WebID card with two SPARQL statements, sent in the body of our HTTP PATCH:


DELETE DATA {<http://localhost:3000/profile/card#me> 
<http://www.w3.org/2006/vcard/ns#fn> "".};
INSERT DATA {<http://localhost:3000/profile/card#me>
<http://www.w3.org/2006/vcard/ns#fn> "Name".};

Assuming everything is sent correctly, this should return a "ResetContent" and update our name.

You can verify that this is working either by again clicking "Get Card Information" or alternatively navigating to "http://localhost:3000/profile/card#me" which should allow you to download your card information by your browser to be opened in the text editor of your choice. This should have now the value that you sent to the Community Solid Server.

Links