Use solid pod as back-end login


#1

I am currently developing an angular web app backed by a node/express server. The back-end is serving some kind of apps which the users can create and publish to other users. I want to use solid pods to store the users data used within the apps. Because i don’t want to burden the users with two different logins, id like to use webid-oidc to use the solid pod for back-end authentication. The problem is that the solid-auth-client and solid-auth-oicd are not designed for back-end usage. I want to implement the following workflow:

  1. Client sends it’s webid to the back-end server to authenticate.
  2. back-end ask solid provider if the user is logged in.
    2.a. user is already logged into the solid provider
    2.b. user is not logged in
    2.b.1 back-end returns 401 Not Authorized
    2.b.2 client application does the login with the solid-auth-client
    2.b.3 continue with step 1

Does exist a best practice to do that or do i have to reassemble that from solid-auth-client and solid-webid-oidc sources? I need just a verification of the webid, the login will be handled by the client application, as my workflow shows.

I would appreciate if you can give me a hint where to start off.


#2

Try https://github.com/jeff-zucker/solid-auth-cli, a node.js version of solid-auth-client with the same API but works without the browser.


#3

Thanks a lot for sharing the code!
No i can use the the solid pod for authorization. But the user has still to type in its credentials two times. Once for access its data in the pod and again for login to my back-end server.

Is there a possibility to reuse a login session across applications? With my current knowledge about webid-oidc I would tend to deny that, because the authorization is stateless. If i get it right the client gets a json-web-token (JWT) from the solid server which is stored in a browser cookie. I could pass the JWT to my back-end, but that would be useless, because to verify the token i need to know the secret which the solid server has used to sign the key. Thus for every resource request to my back-end I would have to sent a authorization request to the solid server to verify the token. That would be a huge performance impact. For sure I could just verify the token once and then just save it on my back-end but then my back-end API would not be stateless anymore. Therefore that is not an option.

Is my assumption right or do I miss something?


#4

Sorry, I misunderstood your work flow. I don’t have any idea if it’s possible to do what you’re looking for.


#5

I have done some further research and it seems, that I have found a solution. Solid uses an asymmetric key pair to sign and validate the JWT. The token gets signed with a private key, but it could be verified with a public key. The public keys are also known as JSON Web Key Set and could be simply retrieved by an API endpoint: “https://…solid-server-domain…/jwks”.

Thus the following workflow should be possible:
Precondition: The Client is not logged in.
Workflow:

  1. Client enters my website.
  2. Client logs into the solid provider according the webid oidc workflow
  3. After successful authentication the client sents a request which needs authentication to my back-end. Its previously acquired token is placed in the http header and passed along with the request.
  4. My back-end server determines the the URL of the solid provider which has issued the token by the iss-claim of the token.
  5. The server request the public key to verify the token and keeps it in its local cache.
  6. The server verifies the token withe the public key and sent a response.

This is still a theoretic solution. If I have got a running example, I will let you know.


#6

I have tested my idea and it is working. Basically I retrieve the jsonwebtoken with a call of currentSession() to the solid-auth-client on the client side. I can use this token then to authorize users within my back-end api. I just put the token as usual in the request header.
On my back-end i can determine the issuer of the token by decoding it. After that I request the public keys from the issuer and verify the token with that keys. Because the token is containing the users webid i can use his webid for access controll to my api endpoints. For the management of the json web keys and the verification of the token I have used thepanva/jose library for node.

The following code is just a schematic example written in TypeScript. The Clientcode is running in an angular web app and the server code is running on a express/node.js server. I have written a brief typedefinion file for the solid-auth-client. Maybe I will publish it in future. But at the moment its is not exhaustive and not tested.

Client Code:

//Import typedefinitions for the solid-auth-client
 import SolidAuthClient from ../../assets/types/solid-auth-client";

//This is a little bit confusing, but it works without an import, because //webpack exposes the authclient at solid.auth

declare namespace solid {
  let auth: SolidAuthClient;
}

solid.auth.currentSession()
  .then((session) => {
    //Get your jsonwebtoken
    let jwt = session.authorization.id_token;
    //<NOW YOU CAN SEND THE TOKEN WITH YOUR REQUESTS TO THE BACK-END>
  });

The typedefinition file for the auth-client:

import {EventEmitter} from 'events
declare type RequestOptions = object;
declare type window = object;

//*******IPC*********
/**
 * Makes remote procedure calls.
 */
export class Client {
    _serverWindow: window;
    _serverOrigin: string;

    constructor(serverWindow: window, serverOrigin: string);

    request(method: string, ...args: any[]): Promise<any>;
}

//*******STORAGE***********
export const NAMESPACE = 'solid-auth-client'

export interface SyncStorage {
    length: number;

    key(index: number): string;

    getItem(key: string): any;

    setItem(key: string, value: any)

    removeItem(key: string);

    clear();
}

export interface AsyncStorage {
    getItem(key: string): Promise<string|null|undefined>;
    setItem(key: string, val: string): Promise<void>;
    removeItem(key: string): Promise<void>;
}

export type Storage = SyncStorage|AsyncStorage

export const defaultStorage: () => AsyncStorage;

/**
 * Gets the deserialized stored data
 */
export function getData(store: Storage): Promise<object>;

/**
 * Updates a Storage object without mutating its intermediate representation.
 */
export function updateStorage(store: Storage, update: (oldData: object) => object): Promise<object>;

/**
 * Takes a synchronous storage interface and wraps it with an async interface.
 */
export function asyncStorage(storage: Storage): AsyncStorage;

export const memStorage : (storage: Storage) => AsyncStorage;

export function ipcStorage(client: Client): AsyncStorage;

//****************SESSION**************
export type webIdOidcSession = {
    idp: string,
    webId: string,
    accessToken: string,
    idToken: string,
    clientId: string,
    sessionKey: string
}

export type Session = webIdOidcSession

export function getSession(storage: AsyncStorage): Promise<Session|null|undefined>;
export function saveSession(storage: AsyncStorage): (session: Session) => Promise<Session>;
export function clearSession(storage: AsyncStorage): Promise<void>


//*********AUTH CLIENT****************
// Store the global fetch, so the user is free to override it
export const globalFetch: object;

export type loginOptions = {
    callbackUri?: string,
    popupUri?: string,
    storage?: any //AsyncStorage
}

export default class SolidAuthClient extends EventEmitter {
    _pendingSession: Promise<Session|null|undefined>|null|undefined;
    fetch(input: RequestInfo, options?: RequestOptions): Promise<Response>;

    login(idp: string, options: loginOptions): Promise<Session|null|undefined>;
    popupLogin(options: loginOptions): Promise <Session|null|undefined >;
    currentSession(storage?: AsyncStorage): Promise <Session|null|undefined>;

    trackSession(callback: (session: Session|null|undefined) => any): Promise <void>;
    logout(storage?: AsyncStorage): Promise <void>;
}

export function defaultLoginOptions(url: string|null|undefined): loginOptions;

ServerCode

import {JWK, JWT} from "@panva/jose";
import * as https from "https";
const { JWKS: { KeyStore } } = require('@panva/jose')

//.....
//<REQUEST WITH THE TOKEN IN THE HEADER>
//<EXTRACT THE TOKEN>
//idToken = ...
//Decode the token and extract the issuer
let decToken = <any>JWT.decode(idToken);
let iss = decToken['iss'];

//Verify the token by requesting the public keys from the issuer
https.get(iss+/jwks", (res) => {
   res.setEncoding('utf8');
   let rawData = '';
   res.on('data', (chunk) => {
         rawData += chunk;
   });
   res.on('end', () => {
     try {
           //Parse the received keys to a JSON-Object
           let jwks = JSON.parse(rawData);
           //Create a keystore containing the keys
           let keyStore = KeyStore.fromJWKS(jwks);
           
           //Validate the token with the public keys
          //The call is successfull if one of the keys match
          //<IMPORTANT Add more claims here to be checked to enhance the security!!!>
           jose.JWT.verify(idToken, keyStore);
          
          //get the users webid
          let webId = decToken['sub'];
         
           } catch (e) {
                 console.error(e.message);
           }
   });
}).on('error', (e) => {
       console.error(`Got error: ${e.message}`);
});