Lab - Using htmlYac as OIDC Client

htmlYac allows flexible scripting by flowing data from one response to another request. We'll use this to try connecting with OIDC Provider

Preparation

  • Simple OIDC Provider
  • htmlYac installed

Launching OIDC Provider

Change the OIDC provider a little:

import express from 'express';
import Provider from 'oidc-provider';

const PORT = 3100

const app = express();

const configuration = {
  clients: [{
     client_id: 'oidccli_id',
     client_secret: 'oidccli_secret',
     grant_types: ['authorization_code'],
     redirect_uris: ['http://localhost:3000/callback'],
     post_logout_redirect_uris: ['http://localhost:3000/'],
     response_types: ['code'],
  }],
  pkce: {
    required: () => false,
  },
  scopes: [
    'email', 'profile',
  ],
  claims: {
    email: ['email'],
    profile: ['first_name'],
  },
  async findAccount(ctx, id) {
    return {
      accountId: id,
      async claims(use, scope) {
        console.log("FIND", id, "USE", use, "SCOPE", scope);
        return { sub: id, email: `${id}@example.com`, first_name: id };
      },
    };
  },
};
const oidc = new Provider(`http://localhost:${PORT}`, configuration);

app.use('/oidc', oidc.callback());

app.listen(PORT, function () {
  console.log(`OIDC is listening on http://localhost:${PORT}`);
});
  • the port is changed from 3000 to 3100 (httpYac uses 3000 for its temp server)
  • no PKCE for simplicity
$ node oidcsrv
OIDC is listening on http://localhost:3100

From VS Code, in the oidc.http file, try the request:

@host=http://localhost:3100
###

GET /oidc/.well-known/openid-configuration

The configuration is displayed:

{
  ...
  "authorization_endpoint": "http://localhost:3100/oidc/auth",
  "end_session_endpoint": "http://localhost:3100/oidc/session/end",
  "grant_types_supported": [
    "implicit",
    "authorization_code"
  ],
  "scopes_supported": [
    "email",
    "profile",
    "openid"
  ],
  "userinfo_endpoint": "http://localhost:3100/oidc/me",
  ...
}

Start the process

GET /oidc
Authorization: oauth2 code local

httpYac will spin up a server asking you to login (to the OIDC provider) and confirm. If successful, we can send the next request:

GET /oidc/me
Authorization: oauth2 code local

The result may look like:

{
  "sub": "john",
  "email": "john@example.com",
  "first_name": "john"
}

Logging out

In VS Code we can open the command palette and find the command "httpYac: Logout User Sessions", but resending the requests above will still succeed since we haven't logged out the OIDC provider.

Since we know the end_session_endpoint from the earlier response, to logout from the OIDC provider, we can issue:

GET http://localhost:3100/oidc/session/end
Authorization: oauth2 code local

We'll see an HTML response, in which a form contains something like:

<form method="post" action="http://localhost:3100/oidc/session/end/confirm">
    <input type="hidden" name="xsrf" value="some_random_key" />
    <input type="hidden" name="logout" value="yes" />
    ...
</form>

We can post the form back to log out by construction another POST request with the required xsrf and logout value. But without doing it manually, we can use httpYac scripting:


{{
    let re = /name="xsrf" value="([\w\d]+)"/
    let m = response.body.match(re)
    exports.xsrf = m[1];
}}

POST http://localhost:3100/oidc/session/end/confirm
Authorization: oauth2 code local
Content-Type: application/x-www-form-urlencoded

logout=yes&xsrf={{ xsrf }}

If it succeeds, we will get some HTML response like

...
<div class="container">
    <h1>Sign-out Success</h1>
    <p>Your sign-out was successful.</p>
</div>

Still, to make sure we are logged out, you may have to run httpYacc: Logout User Sessions in VS Code, and to clear the site data for httpYacc's server (http://localhost:3000).

Using Requests Directly

To look closely what exchanges are made, add logging to the OIDC provider:

import bodyParser from 'body-parser';
...

app.use(bodyParser());

app.use((req, res, next) => {
  console.log('HTTP', req.method, req.url);
  console.log('  headers:', req.headers);
  req.body && console.log('  body:', JSON.stringify(req.body));
});
...

To be continued...