Skip to content
Merged
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
15 changes: 13 additions & 2 deletions src/auth/AuthenticationHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,37 @@ import React from "react";
import BasicAuthHandler from "./BasicAuthHandler";
import BasicTokenAuthHandler from "./BasicTokenAuthHandler";
import CleverAuthHandler from "./CleverAuthHandler";
import EkirjastoAuthHandler from "./EkirjastoAuthHandler";
import SamlAuthHandler from "./SamlAuthHandler";
import {
ClientBasicMethod,
ClientBasicTokenMethod,
ClientEkirjastoMethod,
ClientSamlMethod
} from "interfaces";
import {
BasicAuthType,
CleverAuthType,
CleverAuthMethod,
SamlAuthType,
BasicTokenAuthType
BasicTokenAuthType,
EkirjastoAuthType,
EkirjastoMethod
} from "../types/opds1";
import track from "../analytics/track";
import ApplicationError from "../errors";

type SupportedAuthTypes =
| typeof EkirjastoAuthType
| typeof BasicAuthType
| typeof BasicTokenAuthType
| typeof SamlAuthType
| typeof CleverAuthType;

type SupportedAuthHandlerProps = {
[key in SupportedAuthTypes]: key extends typeof BasicAuthType
[key in SupportedAuthTypes]: key extends typeof EkirjastoAuthType
? EkirjastoMethod
: key extends typeof BasicAuthType
? ClientBasicMethod
: key extends typeof BasicTokenAuthType
? ClientBasicTokenMethod
Expand All @@ -44,6 +51,7 @@ export const authHandlers: {
method: SupportedAuthHandlerProps[key];
}>;
} = {
[EkirjastoAuthType]: EkirjastoAuthHandler,
[BasicAuthType]: BasicAuthHandler,
[BasicTokenAuthType]: BasicTokenAuthHandler,
[SamlAuthType]: SamlAuthHandler,
Expand All @@ -62,6 +70,9 @@ const AuthenticationHandler: React.ComponentType<AuthHandlerWrapperProps> = ({
}) => {
const _AuthHandler = authHandlers[method.type];

if (method.type === EkirjastoAuthType && typeof method !== "string") {
return <_AuthHandler method={method as ClientEkirjastoMethod} />;
}
if (method.type === BasicTokenAuthType && typeof method !== "string") {
return <_AuthHandler method={method as ClientBasicTokenMethod} />;
} else if (method.type === BasicAuthType && typeof method !== "string") {
Expand Down
48 changes: 48 additions & 0 deletions src/auth/EkirjastoAuthHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx } from "theme-ui";
import * as React from "react";
import { ClientEkirjastoMethod } from "interfaces";
import LoadingIndicator from "components/LoadingIndicator";
import Stack from "components/Stack";
import useUser from "components/context/UserContext";
import useLoginRedirectUrl from "auth/useLoginRedirect";
import { clientOnly } from "components/ClientOnly";

/**
* The Ekirjasto Auth handler sends you off to an external website to complete
* auth.
*/
const EkirjastoAuthHandler: React.FC<{ method: ClientEkirjastoMethod }> = ({
method
}) => {
const { token, signOut } = useUser();
const { authSuccessUrl } = useLoginRedirectUrl();

// Get link for strong authentication
const authenticationStartHref = method.links?.find(
link => link.rel === "tunnistus_start"
)?.href;

//Create link with redirect
const urlWithRedirect = `${authenticationStartHref}&redirect_uri=${encodeURIComponent(
authSuccessUrl
)}`;

// Start login
React.useEffect(() => {
if (!token && urlWithRedirect) {
window.location.href = urlWithRedirect;
console.log(urlWithRedirect);
}
}, [token, signOut, urlWithRedirect]);

return (
<Stack direction="column" sx={{ alignItems: "center" }}>
<LoadingIndicator />
Logging in with EkirjastoAuthentication...
</Stack>
);
};

export default clientOnly(EkirjastoAuthHandler);
11 changes: 10 additions & 1 deletion src/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Text } from "components/Text";
import LoadingIndicator from "components/LoadingIndicator";
import useLogin from "auth/useLogin";
import { isSupportedAuthType } from "./AuthenticationHandler";
import { EKIRJASTO_AUTH_TYPE } from "utils/constants";

export default function Login(): React.ReactElement {
const { initLogin } = useLogin();
Expand All @@ -20,9 +21,17 @@ export default function Login(): React.ReactElement {
);

// Automatically redirect user to first supported auth method
// Unless there is ekirjasto authentication, then redirect to that
React.useEffect(() => {
if (supportedAuthMethods.length > 0) {
initLogin(supportedAuthMethods[0].id);
const ekirjastoAuth = supportedAuthMethods.find(
method => method.type === EKIRJASTO_AUTH_TYPE
);
if (ekirjastoAuth) {
initLogin(ekirjastoAuth.id);
} else {
initLogin(supportedAuthMethods[0].id);
}
}
}, [supportedAuthMethods, initLogin]);

Expand Down
26 changes: 26 additions & 0 deletions src/auth/ekirjastoFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ApplicationError, { ServerError } from "errors";
import fetchWithHeaders from "../dataflow/fetch";

/**
* Fetch bearer (circulation) token for authenticating user
*/
export async function fetchEAuthToken(
url: string | undefined,
token: string | undefined
) {
if (!url) {
throw new ApplicationError({
title: "Incomplete Authentication Info",
detail: "No URL or Token was provided for authentication"
});
}

const response = await fetchWithHeaders(url, `Bearer ${token}`, {}, "POST");
const json = await response.json();

if (!response.ok) {
throw new ServerError(url, response.status, json);
}

return json;
}
85 changes: 71 additions & 14 deletions src/auth/useCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as React from "react";
import Cookie from "js-cookie";
import { AuthCredentials, OPDS1 } from "interfaces";
import { AppAuthMethod, AuthCredentials, OPDS1 } from "interfaces";
import { IS_SERVER } from "utils/env";
import { NextRouter, useRouter } from "next/router";
import { generateCredentials } from "utils/auth";
import { SAML_LOGIN_QUERY_PARAM } from "utils/constants";
import {
EKIRJASTO_AUTH_TYPE,
EKIRJASTO_TOKEN_PARAM,
SAML_LOGIN_QUERY_PARAM
} from "utils/constants";

/**
* This hook:
Expand All @@ -16,16 +20,31 @@ import { SAML_LOGIN_QUERY_PARAM } from "utils/constants";
* if finds a token, it extracts it and sets it as the current
* credentials.
*/
export default function useCredentials(slug: string | null) {
export default function useCredentials(
slug: string | null,
authMethods: AppAuthMethod[] | null
) {
const router = useRouter();

// Since we don't actually call the login function anywhere, we need to put the authentication url
// in somehow, so we fetch it here and take it to getCredentialsState function
const ekirjastoMethod = authMethods?.find(
method => method.type === EKIRJASTO_AUTH_TYPE
);
let authenticationUrl;
if (ekirjastoMethod) {
authenticationUrl = ekirjastoMethod.links?.find(
link => link.rel === "authenticate"
)?.href;
}
const [credentialsState, setCredentialsState] = React.useState<
AuthCredentials | undefined
>(getCredentialsCookie(slug));
>(getCredentialsCookie(slug, authenticationUrl));
// sync up cookie state with react state
React.useEffect(() => {
const cookie = getCredentialsCookie(slug);
const cookie = getCredentialsCookie(slug, authenticationUrl);
if (cookie) setCredentialsState(cookie);
}, [slug]);
}, [authenticationUrl, slug]);

// set both cookie and state credentials
const setCredentials = React.useCallback(
Expand Down Expand Up @@ -60,7 +79,7 @@ export default function useCredentials(slug: string | null) {
}

/**
* COOKIE CREDENDIALS
* COOKIE CREDENTIALS
*/
/**
* If you pass a librarySlug, the cookie will be scoped to the
Expand All @@ -70,31 +89,69 @@ function cookieName(librarySlug: string | null): string {
const AUTH_COOKIE_NAME = "CPW_AUTH_COOKIE";
return `${AUTH_COOKIE_NAME}/${librarySlug}`;
}

/**
* When using ekirjasto authentication, we don't use scoping to a particular library
* @returns ekirjasto cookie name
*/
function cookieNameEkirjasto(): string {
const AUTH_COOKIE_NAME = EKIRJASTO_TOKEN_PARAM;
return AUTH_COOKIE_NAME;
}
/**
* Get credentials from cookies.
* We assume the token is in access_token cookie, and that it is
* of the Ekirjasto Authentication type
*
* @param librarySlug Library slug, that is useful if we have multiple libraries
* @param authenticationUrl AuthenticationUrl where we make refresh requests
* @returns Ekirjasto credentials if access_token is available, otherwise undefined
*/
function getCredentialsCookie(
librarySlug: string | null
librarySlug: string | null,
authenticationUrl: string | null
): AuthCredentials | undefined {
const credentials = Cookie.get(cookieName(librarySlug));
return credentials ? JSON.parse(credentials) : undefined;
if (librarySlug === "ekirjasto") {
// Get access token, for ekirjasto login credentials
const accessToken = Cookie.get(cookieNameEkirjasto());
// Create ekirjasto authentication credentials
const authCredentials: AuthCredentials = {
token: `Bearer ${accessToken}`,
methodType: OPDS1.EkirjastoAuthType,
authenticationUrl: authenticationUrl ? authenticationUrl : undefined
};
// Return the credentials
return authCredentials ? authCredentials : undefined;
} else {
const credentials = Cookie.get(cookieName(librarySlug));
return credentials ? JSON.parse(credentials) : undefined;
}
}

function setCredentialsCookie(
librarySlug: string | null,
credentials: AuthCredentials
) {
Cookie.set(cookieName(librarySlug), JSON.stringify(credentials));
if (librarySlug === "ekirjasto") {
Cookie.set(cookieNameEkirjasto(), JSON.stringify(credentials));
} else {
Cookie.set(cookieName(librarySlug), JSON.stringify(credentials));
}
}

function clearCredentialsCookie(librarySlug: string | null) {
Cookie.remove(cookieName(librarySlug));
if (librarySlug === "ekirjasto") {
Cookie.remove(cookieNameEkirjasto());
} else {
Cookie.remove(cookieName(librarySlug));
}
}

export function generateToken(username: string, password?: string) {
return generateCredentials(username, password);
}

/**
* URL CREDENTIALS
* URL CREDENTIALS, NOT USED WITH EKIRJASTO
*/
function getUrlCredentials(router: NextRouter) {
/* TODO: throw error if samlAccessToken and cleverAccessToken exist at the same time as this is an invalid state that shouldn't be reached */
Expand Down
10 changes: 8 additions & 2 deletions src/auth/useLoginRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IS_SERVER } from "utils/env";
/**
* Extracts the redirect url stored in the browser url bar and returns
* a version including the origin (fullSuccessUrl) or a version that is
* just a path (successPath).
* just a path (successPath), or a path for special authentication handling (authSuccessUrl)
*/
export default function useLoginRedirectUrl() {
const { buildMultiLibraryLink } = useLinkUtils();
Expand All @@ -33,8 +33,14 @@ export default function useLoginRedirectUrl() {
? ""
: `${window.location.origin}${successPath}`;

// get the url where we redirect after authentication
const authSuccessUrl = IS_SERVER
? ""
: `${window.location.origin}/api/authsuccess`;

return {
fullSuccessUrl,
successPath
successPath,
authSuccessUrl
};
}
11 changes: 8 additions & 3 deletions src/components/context/UserContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ServerError } from "errors";
import { AppAuthMethod, AnyBook, AuthCredentials, Token } from "interfaces";
import * as React from "react";
import useSWR from "swr";
import { BasicTokenAuthType } from "types/opds1";
import { BasicTokenAuthType, EkirjastoAuthType } from "types/opds1";
import { addHours, isBefore } from "date-fns";

type Status = "authenticated" | "loading" | "unauthenticated";
Expand Down Expand Up @@ -44,9 +44,10 @@ interface UserProviderProps {
* those change it will cause a refetch.
*/
export const UserProvider = ({ children }: UserProviderProps) => {
const { shelfUrl, slug } = useLibraryContext();
const { shelfUrl, slug, authMethods } = useLibraryContext();
const { credentials, setCredentials, clearCredentials } = useCredentials(
slug
slug,
authMethods
);
const [error, setError] = React.useState<ServerError | null>(null);

Expand Down Expand Up @@ -97,6 +98,10 @@ export const UserProvider = ({ children }: UserProviderProps) => {
clearCredentials();
}
}
if (credentials?.methodType === EkirjastoAuthType) {
// TODO: token refresh on 401
console.log("EKIRJASTO REFRESH");
}
}
},
// clear credentials whenever we receive a 401, but save the error so it sticks around.
Expand Down
7 changes: 6 additions & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,17 @@ export interface ClientBasicTokenMethod extends OPDS1.BasicTokenAuthMethod {
id: string;
}

export interface ClientEkirjastoMethod extends OPDS1.EkirjastoMethod {
id: string;
}

// auth methods once they have been processed for the app
export type AppAuthMethod =
| ClientCleverMethod
| ClientBasicMethod
| ClientBasicTokenMethod
| ClientSamlMethod;
| ClientSamlMethod
| ClientEkirjastoMethod;

export type Token = {
basicToken: string | undefined;
Expand Down
Loading