Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/frontend/packages/auth-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@semapps/semantic-data-provider": "1.2.0",
"@inrupt/solid-client-authn-browser": "^3.1.1",
"classnames": "^2.3.2",
"jwt-decode": "^3.0.0",
"lodash": "^4.17.21",
Expand Down
200 changes: 200 additions & 0 deletions src/frontend/packages/auth-provider/src/solidAuthProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import jwtDecode from 'jwt-decode';
import * as oauth from 'oauth4webapi';
import { SemanticDataProvider } from '@semapps/semantic-data-provider';
import { AuthProvider, UserIdentity } from 'react-admin';

interface SolidAuthProviderSettings {
dataProvider: SemanticDataProvider;
allowAnonymous?: boolean;
checkUser?: (userData: any) => boolean;
clientId: string;
}

const solidAuthProvider = ({
dataProvider,
allowAnonymous = true,
checkUser,
clientId
}: SolidAuthProviderSettings): AuthProvider => ({
login: async (params: any) => {
let { webId, issuer, redirect = '/', isSignup = false } = params;

if (webId && !issuer) {
// Find issuer from webId
const { json: userData } = await dataProvider.fetch(webId);
if (!userData) throw new Error('auth.message.unable_to_fetch_user_data');
if (!userData['solid:oidcIssuer']) throw new Error('auth.message.no_associated_oidc_issuer');
issuer = userData?.['solid:oidcIssuer'];
}

const as = await oauth
.discoveryRequest(new URL(issuer))
.then(response => oauth.processDiscoveryResponse(new URL(issuer), response))
.catch(() => {
throw new Error('auth.message.unreachable_auth_server');
});

const codeVerifier = oauth.generateRandomCodeVerifier();
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
const codeChallengeMethod = 'S256';

// Save to use on handleCallback method
localStorage.setItem('code_verifier', codeVerifier);
localStorage.setItem('redirect', redirect);

const authorizationUrl = new URL(as.authorization_endpoint!);
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', clientId);
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
authorizationUrl.searchParams.set('redirect_uri', `${window.location.origin}/auth-callback`);
authorizationUrl.searchParams.set('scope', 'openid webid offline_access');
authorizationUrl.searchParams.set('is_signup', isSignup);

// @ts-expect-error TS(2322): Type 'URL' is not assignable to type '(string | Lo... Remove this comment to see the full error message
window.location = authorizationUrl;
},
handleCallback: async () => {
const { searchParams } = new URL(window.location.href);

// @ts-expect-error TS(2345): Argument of type 'string | null' is not assignable... Remove this comment to see the full error message
const issuer = new URL(searchParams.get('iss'));
const as = await oauth.discoveryRequest(issuer).then(response => oauth.processDiscoveryResponse(issuer, response));

const client = {
client_id: clientId,
token_endpoint_auth_method: 'none' // We don't have a client secret
} as oauth.Client;

const currentUrl = new URL(window.location.href);
const params = oauth.validateAuthResponse(as, client, currentUrl, oauth.expectNoState);
if (oauth.isOAuth2Error(params)) {
throw new Error(`OAuth error: ${params.error} (${params.error_description})`);
}

// Retrieve data set during login
const codeVerifier = localStorage.getItem('code_verifier')!;
const redirect = localStorage.getItem('redirect');

const response = await oauth.authorizationCodeGrantRequest(
as,
client,
params,
`${window.location.origin}/auth-callback`,
codeVerifier
);

const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response);
if (oauth.isOAuth2Error(result)) {
// @ts-expect-error
throw new Error(`OAuth error: ${params.error} (${params.error_description})`);
}

// Until DPoP is implemented, use the ID token to log into local Pod
// And the proxy endpoint to log into remote Pods
localStorage.setItem('token', result.id_token);

// Remove we don't need it anymore
localStorage.removeItem('code_verifier');
localStorage.removeItem('redirect');

// Reload to ensure the dataServer config is reset
window.location.href = redirect || '/';
},
signup: async (params: any) => {
// Not implemented ?
},
logout: async (params: any) => {
const { redirectUrl } = params || {};

const token = localStorage.getItem('token');
if (token) {
const { webid: webId }: any = jwtDecode(token); // Not webId !!

// Delete token but also any other value in local storage
localStorage.clear();

if (redirectUrl) {
return redirectUrl;
} else {
// We don't need the token to fetch the WebID since it is public
const { json: userData } = await dataProvider.fetch(webId);

// Redirect to the Pod provider
return userData?.['solid:oidcIssuer'] || new URL(webId).origin;
}
} else {
return redirectUrl;
}
},
checkAuth: async () => {
const token = localStorage.getItem('token');
if (!token && !allowAnonymous) throw new Error();
},
checkUser: (userData: any) => {
if (checkUser) {
return checkUser(userData);
}
return true;
},
checkError: (error: any) => {
// We want to disconnect only with INVALID_TOKEN errors
if (error.status === 401 && error.body && error.body.type === 'INVALID_TOKEN') {
localStorage.removeItem('token');
return Promise.reject();
} else {
// Other error code (404, 500, etc): no need to log out
return Promise.resolve();
}
},
getIdentity: async () => {
const token = localStorage.getItem('token');
if (token) {
const payload = jwtDecode(token);

const webId = payload.webid; // Not webId !!

if (!webId) {
// If webId is not set, it is probably because we have ActivityPods v1 tokens and we need to disconnect
localStorage.removeItem('token');
window.location.href = '/login';
throw new Error('No webId found on provided token !');
}

const { json: webIdData } = await dataProvider.fetch(webId);
let profileData = {};

if (webIdData.url) {
try {
const { status, json } = await dataProvider.fetch(webIdData.url);
if (status === 200) profileData = json;
} catch (e) {
// Could not fetch profile. Continue...
console.error(e);
}
}

return {
id: webId,
fullName:
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
profileData['vcard:given-name'] ||
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
profileData['pair:label'] ||
webIdData['foaf:name'] ||
webIdData['pair:label'],
avatar:
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
profileData['vcard:photo'] ||
webIdData.image?.url ||
webIdData.image ||
webIdData.icon?.url ||
webIdData.icon,
profileData,
webIdData
} as UserIdentity;
}
}
});

export default solidAuthProvider;
111 changes: 111 additions & 0 deletions src/frontend/packages/auth-provider/src/solidAuthProvider2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { login, logout, getDefaultSession, handleIncomingRedirect, fetch } from '@inrupt/solid-client-authn-browser';
import { SemanticDataProvider } from '@semapps/semantic-data-provider';
import { AuthProvider, UserIdentity } from 'react-admin';

interface SolidAuthProviderSettings {
dataProvider: SemanticDataProvider;
allowAnonymous?: boolean;
checkUser?: (userData: any) => boolean;
clientId: string;
clientName: string;
}

const solidAuthProvider = ({
dataProvider,
allowAnonymous = true,
checkUser,
clientId,
clientName
}: SolidAuthProviderSettings): AuthProvider => ({
login: async (params: any) => {
let { webId, issuer, redirect = '/', isSignup = false } = params;

// if (webId && !issuer) {
// // Find issuer from webId
// const { json: userData } = await dataProvider.fetch(webId);
// if (!userData) throw new Error('auth.message.unable_to_fetch_user_data');
// if (!userData['solid:oidcIssuer']) throw new Error('auth.message.no_associated_oidc_issuer');
// issuer = userData?.['solid:oidcIssuer'];
// }

localStorage.setItem('redirect', redirect);

await login({
oidcIssuer: issuer,
redirectUrl: `${window.location.origin}/auth-callback`,
clientName
// customScopes: ['openid', 'webid', 'offline_access']
});
},
handleCallback: async () => {
const redirect = localStorage.getItem('redirect');

// Remove we don't need it anymore
localStorage.removeItem('redirect');

await handleIncomingRedirect();

// Reload to ensure the dataServer config is reset
window.location.href = redirect || '/';
},
signup: async (params: any) => {
// Not implemented ?
},
logout: async (params: any) => {
const { redirectUrl }: { redirectUrl: string } = params || {};

await logout({ logoutType: 'idp', postLogoutUrl: redirectUrl });
},
checkAuth: async () => {
const isLogged = !getDefaultSession().info.isLoggedIn;
if (!isLogged && !allowAnonymous) throw new Error();
},
checkUser: (userData: any) => {
if (checkUser) {
return checkUser(userData);
}
return true;
},
checkError: (error: any) => {
// We want to disconnect only with INVALID_TOKEN errors
if (error.status === 401 && error.body && error.body.type === 'INVALID_TOKEN') {
localStorage.removeItem('token');
return Promise.reject();
} else {
// Other error code (404, 500, etc): no need to log out
return Promise.resolve();
}
},
getIdentity: async (): Promise<UserIdentity> => {
let { webId } = getDefaultSession().info;
let webIdData: any = {};
let profileData: any = {};

if (webId) {
const response = await fetch(webId, { headers: new Headers({ 'Content-Type': 'application/ld+json' }) });
webIdData = await response.json();

if (webIdData.url) {
const response = await fetch(webIdData.url, {
headers: new Headers({ 'Content-Type': 'application/ld+json' })
});
profileData = await response.json();
}
}

return {
id: webId,
fullName:
profileData['vcard:given-name'] ||
profileData['pair:label'] ||
webIdData['foaf:name'] ||
webIdData['pair:label'],
avatar:
profileData['vcard:photo'] || webIdData.image?.url || webIdData.image || webIdData.icon?.url || webIdData.icon,
profileData,
webIdData
};
}
});

export default solidAuthProvider;
30 changes: 30 additions & 0 deletions src/frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3376,6 +3376,16 @@
jose "^5.1.3"
uuid "^11.1.0"

"@inrupt/oidc-client-ext@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@inrupt/oidc-client-ext/-/oidc-client-ext-3.1.1.tgz#49ceaae936d63a48bdffc75f8ab06446505b5b47"
integrity sha512-vftKD2u5nufZTFkdUDMS3Uxj5xNQwArP11OFaALFkq6/3RwCAhe3lwOv8hNzL7Scv98T+KbAErBM0TwGGrS69g==
dependencies:
"@inrupt/oidc-client" "^1.11.6"
"@inrupt/solid-client-authn-core" "^3.1.1"
jose "^5.1.3"
uuid "^11.1.0"

"@inrupt/oidc-client@^1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@inrupt/oidc-client/-/oidc-client-1.11.6.tgz#5b1ba7e84d0ca4da356f44db1ae8056881502027"
Expand All @@ -3398,6 +3408,17 @@
jose "^5.1.3"
uuid "^11.1.0"

"@inrupt/solid-client-authn-browser@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@inrupt/solid-client-authn-browser/-/solid-client-authn-browser-3.1.1.tgz#ac9661e1e694fedc9443c1de13e2010c4e2429b4"
integrity sha512-Wd7TREmvdhTp+Sk88ei3hlg54sG1fNqkkPkuS+2tDBkcsXaViRQAEugVyh5pWRkd1xSFKrEzftb7UYEG4mJ0CQ==
dependencies:
"@inrupt/oidc-client-ext" "^3.1.1"
"@inrupt/solid-client-authn-core" "^3.1.1"
events "^3.3.0"
jose "^5.1.3"
uuid "^11.1.0"

"@inrupt/solid-client-authn-core@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@inrupt/solid-client-authn-core/-/solid-client-authn-core-3.1.0.tgz#18ecec3a4e1dad8c0f7daa53359a499fd8024dee"
Expand All @@ -3407,6 +3428,15 @@
jose "^5.1.3"
uuid "^11.1.0"

"@inrupt/solid-client-authn-core@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@inrupt/solid-client-authn-core/-/solid-client-authn-core-3.1.1.tgz#cd4ae6a6f32cb231a922d005034badab88df9c0c"
integrity sha512-1oDSQCh/pVtPlTyvLQ2uwHo+hpLJF7izg82tjB+Ge8jqGYwkQyId0BrfncpCk//uJXxgRIcfAQp2MhXYbZo80Q==
dependencies:
events "^3.3.0"
jose "^5.1.3"
uuid "^11.1.0"

"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
Expand Down