mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00

The biggest resulting change is that the consent page always appears after logging in. Some minor fixes to be closer to the spec are included together with some minor structural refactors.
139 lines
5.0 KiB
TypeScript
139 lines
5.0 KiB
TypeScript
import { stringify } from 'querystring';
|
|
import { URL } from 'url';
|
|
import { Session } from '@inrupt/solid-client-authn-node';
|
|
import { load } from 'cheerio';
|
|
import type { Response } from 'cross-fetch';
|
|
import { fetch } from 'cross-fetch';
|
|
import type { Cookie } from 'set-cookie-parser';
|
|
import { parse, splitCookiesString } from 'set-cookie-parser';
|
|
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
|
|
|
|
/* eslint-disable jest/no-standalone-expect */
|
|
/**
|
|
* Helper class to track the state while going through an IDP procedure
|
|
* and generalizes several common calls and checks.
|
|
*/
|
|
export class IdentityTestState {
|
|
private readonly baseUrl: string;
|
|
private readonly redirectUrl: string;
|
|
private readonly oidcIssuer: string;
|
|
|
|
public readonly session: Session;
|
|
private readonly cookies: Map<string, Cookie>;
|
|
private cookie?: string;
|
|
|
|
public constructor(baseUrl: string, redirectUrl: string, oidcIssuer: string) {
|
|
this.baseUrl = baseUrl;
|
|
this.redirectUrl = redirectUrl;
|
|
this.oidcIssuer = oidcIssuer;
|
|
this.session = new Session();
|
|
this.cookies = new Map();
|
|
}
|
|
|
|
/**
|
|
* Performs a fetch call while keeping track of the stored cookies and preventing redirects.
|
|
* @param url - URL to call.
|
|
* @param method - Method to use.
|
|
* @param body - Body to send along.
|
|
* @param contentType - Content-Type of the body.
|
|
*/
|
|
public async fetchIdp(url: string, method = 'GET', body?: string, contentType?: string): Promise<Response> {
|
|
const options = { method, headers: { cookie: this.cookie }, body, redirect: 'manual' } as any;
|
|
if (contentType) {
|
|
options.headers['content-type'] = contentType;
|
|
}
|
|
const res = await fetch(url, options);
|
|
|
|
// Parse the cookies that need to be set and convert them to the corresponding header value
|
|
// Make sure we don't overwrite cookies that were already present
|
|
if (res.headers.get('set-cookie')) {
|
|
const newCookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
|
|
for (const cookie of newCookies) {
|
|
this.cookies.set(cookie.name, cookie);
|
|
}
|
|
// eslint-disable-next-line unicorn/prefer-spread
|
|
this.cookie = Array.from(this.cookies, ([ , nom ]): string => `${nom.name}=${nom.value}`).join('; ');
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Uses the given jquery command to find a node in the given html body.
|
|
* The value from the given attribute field then gets extracted and combined with the base url.
|
|
* @param html - Body to parse.
|
|
* @param jquery - Query to run on the body.
|
|
* @param attr - Attribute to extract.
|
|
*/
|
|
public extractUrl(html: string, jquery: string, attr: string): string {
|
|
const url = load(html)(jquery).attr(attr);
|
|
expect(typeof url).toBe('string');
|
|
return new URL(url!, this.baseUrl).href;
|
|
}
|
|
|
|
/**
|
|
* Initializes an authentication session and stores the relevant cookies for later re-use.
|
|
* All te relevant links from the login page get extracted.
|
|
*/
|
|
public async startSession(clientId?: string): Promise<string> {
|
|
let nextUrl = '';
|
|
await this.session.login({
|
|
redirectUrl: this.redirectUrl,
|
|
oidcIssuer: this.oidcIssuer,
|
|
clientId,
|
|
handleRedirect(data): void {
|
|
nextUrl = data;
|
|
},
|
|
});
|
|
expect(nextUrl.length > 0).toBeTruthy();
|
|
expect(nextUrl.startsWith(this.oidcIssuer)).toBeTruthy();
|
|
|
|
// Need to catch the redirect so we can copy the cookies
|
|
let res = await this.fetchIdp(nextUrl);
|
|
expect(res.status).toBe(303);
|
|
nextUrl = res.headers.get('location')!;
|
|
|
|
// Handle redirect
|
|
res = await this.fetchIdp(nextUrl);
|
|
expect(res.status).toBe(200);
|
|
|
|
// Need to send request to prompt API to get actual location
|
|
let json = await res.json();
|
|
res = await this.fetchIdp(json.controls.prompt);
|
|
json = await res.json();
|
|
nextUrl = json.location;
|
|
|
|
return nextUrl;
|
|
}
|
|
|
|
/**
|
|
* Logs in by sending the corresponding email and password to the given form action.
|
|
* The URL should be extracted from the login page.
|
|
*/
|
|
public async login(url: string, email: string, password: string): Promise<string> {
|
|
const formData = stringify({ email, password });
|
|
let res = await this.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
|
|
expect(res.status).toBe(200);
|
|
const json = await res.json();
|
|
res = await this.fetchIdp(json.location);
|
|
expect(res.status).toBe(303);
|
|
return res.headers.get('location')!;
|
|
}
|
|
|
|
/**
|
|
* Handles the consent screen at the given URL and the followup redirect back to the client.
|
|
*/
|
|
public async consent(url: string): Promise<void> {
|
|
let res = await this.fetchIdp(url, 'POST', '', APPLICATION_X_WWW_FORM_URLENCODED);
|
|
expect(res.status).toBe(200);
|
|
const json = await res.json();
|
|
|
|
res = await this.fetchIdp(json.location);
|
|
expect(res.status).toBe(303);
|
|
const mockUrl = res.headers.get('location')!;
|
|
expect(mockUrl.startsWith(this.redirectUrl)).toBeTruthy();
|
|
|
|
const info = await this.session.handleIncomingRedirect(mockUrl);
|
|
expect(info?.isLoggedIn).toBe(true);
|
|
}
|
|
}
|