Skip to content

Commit

Permalink
use credentials to authenticate
Browse files Browse the repository at this point in the history
  • Loading branch information
craigrbarnes committed Mar 1, 2024
1 parent 5e879e1 commit bc87e1c
Show file tree
Hide file tree
Showing 18 changed files with 198 additions and 28 deletions.
2 changes: 1 addition & 1 deletion docs/Overview/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Top level configuration is stored in .env files. These files contain the followi
# For example, if commons is set to gen3, the configurations in directory config/gen3 will be used
NEXT_PUBLIC_COMMONS=gen3
NEXT_PUBLIC_GEN3_API=https://localhost:3010
NEXT_PUBLIC_GEN3_DOMAIN=https://localhost:3010
NEXT_PUBLIC_GEN3_DOMAIN=localhost:3010
````

The value of `NEXT_PUBLIC_COMMONS` is used to determine the name of the commons-specific configuration file to use.
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/features/authz/authStateSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CoreState } from '../../reducers';

export interface AuthState {
csrf?: string;
accessToken?: string;
}

const initialState : AuthState = {
csrf: undefined,
accessToken: undefined,
};

const slice = createSlice({
name: 'authState',
initialState,
reducers: {
setCSRF: (
state: AuthState,
action: PayloadAction<{ csrf: string }>,
) => {
return { ...state, csrf: action.payload.csrf };
},
setAccessToken: (state: AuthState, action: PayloadAction<{ accessToken: string }>) => {
return { ...state, accessToken: action.payload.accessToken };
},
},
});


export const authReducer = slice.reducer;
export const { setCSRF, setAccessToken } = slice.actions;

export const selectAuthCSRF = (state: CoreState): string | undefined =>
state.auth.csrf;

export const selectAccessToken = (state: CoreState): string | undefined =>
state.auth.accessToken;
3 changes: 2 additions & 1 deletion packages/core/src/features/authz/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type AuthzMapping, type ServiceAndMethod } from './types';
import { useGetAuthzMappingsQuery } from './authzMappingSlice';
import { setAccessToken, setCSRF, selectAccessToken, selectAuthCSRF } from './authStateSlice';

export { useGetAuthzMappingsQuery, type AuthzMapping, type ServiceAndMethod };
export { useGetAuthzMappingsQuery, type AuthzMapping, type ServiceAndMethod, setCSRF, setAccessToken, selectAccessToken, selectAuthCSRF};
26 changes: 24 additions & 2 deletions packages/core/src/features/fence/credentialsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ interface DeleteCredentialParams {
readonly id: string;
}

interface AuthorizeFromCredentialsParams {
api_key: string;
key_id: string;
}
export interface AuthTokenResponse {
access_token: string;
}

export const credentialsApi = credentialsWithTags.injectEndpoints({
endpoints: (builder) => ({
getCredentials: builder.query<ReadonlyArray<APIKey>, void>({
Expand Down Expand Up @@ -49,16 +57,30 @@ export const credentialsApi = credentialsWithTags.injectEndpoints({
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...(csrfToken && {'x-csrf-token': csrfToken}),
...(csrfToken && { 'x-csrf-token': csrfToken }),
},
}),
invalidatesTags: ['Credentials'],
}),
}),
authorizeFromCredentials: builder.mutation<AuthTokenResponse, AuthorizeFromCredentialsParams>({
query: (params) => ({
url: '/user/credentials/api/access_token',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: {
api_key: params.api_key,
key_id: params.key_id,
},
}),
}),
})
});

export const {
useGetCredentialsQuery,
useAddNewCredentialMutation,
useRemoveCredentialMutation,
useAuthorizeFromCredentialsMutation
} = credentialsApi;
4 changes: 3 additions & 1 deletion packages/core/src/features/fence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
useGetCredentialsQuery,
useAddNewCredentialMutation,
useRemoveCredentialMutation,
useAuthorizeFromCredentialsMutation
} from './credentialsApi';

import {
Expand All @@ -33,5 +34,6 @@ export {
useAddNewCredentialMutation,
useRemoveCredentialMutation,
useGetLoginProvidersQuery,
useGetJWKKeysQuery
useGetJWKKeysQuery,
useAuthorizeFromCredentialsMutation
};
2 changes: 0 additions & 2 deletions packages/core/src/features/fence/jwtApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export const jwtApi = credentialsWithTags.injectEndpoints({
query: () => 'user/jwt/keys',
providesTags: ['fenceJWT'],
}),


}),
});

Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/features/gen3/gen3Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ export const gen3Api = coreCreateApi({
baseQuery: fetchBaseQuery({
baseUrl: `${GEN3_API}`,
prepareHeaders: (headers, { getState }) => {
const state = getState() as CoreState;
const csrfToken = selectCSRFToken(getState() as CoreState);
console.log("gen3Serivces: ", state.auth);
const accessToken = state.auth.accessToken;

Check failure on line 20 in packages/core/src/features/gen3/gen3Api.ts

View workflow job for this annotation

GitHub Actions / build

Use object destructuring
if (csrfToken) {
headers.set('X-CSRFToken', csrfToken);
}
if (accessToken) {
headers.set('Authorization', `Bearer ${accessToken}`);
}
headers.set('Content-Type', 'application/json');
return headers;
},
Expand All @@ -39,13 +45,16 @@ export const { useGetCSRFQuery } = gen3Api;

export const selectCSRFTokenData = gen3Api.endpoints.getCSRF.select();


const passThroughTheState = (state: CoreState) => state.gen3Services;

export const selectCSRFToken = createSelector(
[selectCSRFTokenData, passThroughTheState],
(state) => state?.data?.csrfToken,
);



export const selectHeadersWithCSRFToken = createSelector(
[selectCSRFToken, passThroughTheState],
(csrfToken) => ({
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { gen3AppReducer } from './features/gen3Apps/gen3AppsSlice';
import { drsHostnamesReducer } from './features/drsResolver';
import { modalReducer } from './features/modals/modalsSlice';
import { cohortReducer } from './features/cohort';
import { authReducer } from './features/authz/authStateSlice';

import {
guppyApiReducer,
Expand All @@ -18,6 +19,7 @@ export const rootReducer = combineReducers({
drsHostnames: drsHostnamesReducer,
modals: modalReducer,
cohorts: cohortReducer,
auth: authReducer,
[guppyApiSliceReducerPath]: guppyApiReducer,
});

Expand Down
70 changes: 70 additions & 0 deletions packages/frontend/src/components/Login/CredentialsLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, {useState} from 'react';
import { Button, Loader, TextInput } from '@mantine/core';
import { GEN3_DOMAIN, setAccessToken, useAuthorizeFromCredentialsMutation, useCoreDispatch } from '@gen3/core';
import { MdClose as CloseIcon } from 'react-icons/md';
import { useDeepCompareEffect } from 'use-deep-compare';
import { useRouter } from 'next/router';
import { showNotification } from '@mantine/notifications';
import { stripTrailingSlash } from '../../utils';
import { LoginRedirectProps } from './types';

const CredentialsLogin = ( { handleLoginSelected, redirectURL } : LoginRedirectProps) => {

const dispatch = useCoreDispatch();

const [credentials, setCredentials] = useState('');

const [authorizeFromCredentials, { data, isError, isLoading }] = useAuthorizeFromCredentialsMutation();

useDeepCompareEffect( () => {
if (data?.access_token) {
fetch('/api/auth/setSessionToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ access_token: data.access_token })
});

dispatch(setAccessToken({ accessToken: data?.access_token }));
handleLoginSelected('', redirectURL);
}}, [data]);

return (
<div className='flex w-full'>
<TextInput value={credentials}
onChange={(event) => {
setCredentials(event.target.value);
}}

classNames={{
input: 'focus:border-2 focus:border-primary text-sm',
}}
size="sm"
rightSection={
isLoading ? (
<Loader size={20} />
) :
credentials.length > 0 && (
<CloseIcon
onClick={() => {
setCredentials('');
}}
className="cursor-pointer"
data-testid="search-input-clear-search"
/>
)
}
></TextInput>
<Button color="blue" onClick={() => {
const payload = {
"api_key": "",
"key_id": ""
};
authorizeFromCredentials(payload);
}}>Authorize</Button>
</div>
);
};

export default CredentialsLogin;
14 changes: 7 additions & 7 deletions packages/frontend/src/components/Login/LoginPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,11 @@ import { useRouter } from 'next/router';
import { showNotification } from '@mantine/notifications';
import TexturedSidePanel from '../Layout/TexturedSidePanel';
import LoginProvidersPanel from './LoginProvidersPanel';
import CredentialsLogin from './CredentialsLogin';
import TextContent from '../Content/TextContent';
import { LoginConfig } from './types';
import { GEN3_DOMAIN } from '@gen3/core';

const stripTrailingSlash = (str:string):string => {
return str.endsWith('/') ?
str.slice(0, -1) :
str;
};

import { stripTrailingSlash } from '../../utils'; // stripTrailingSlash is not used in this file

const LoginPanel = (loginConfig: LoginConfig) => {
const { image, topContent, bottomContent } = loginConfig;
Expand Down Expand Up @@ -52,6 +47,11 @@ const LoginPanel = (loginConfig: LoginConfig) => {
redirectURL={redirectURL as string | undefined}
/>

<CredentialsLogin
handleLoginSelected={handleLoginSelected}
redirectURL={redirectURL as string | undefined}
/>

{bottomContent?.map((content, index) => (
<TextContent {...content} key={index} />
))}
Expand Down
16 changes: 6 additions & 10 deletions packages/frontend/src/components/Login/LoginProvidersPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import React, {useState} from 'react';
import React, { useState } from 'react';
import { Box, Button, LoadingOverlay, Select, Stack } from '@mantine/core';
import { useGetLoginProvidersQuery } from '@gen3/core';
import type { Gen3LoginProvider, NameUrl } from '@gen3/core';
import { useGetLoginProvidersQuery } from '@gen3/core';
import { LoginRedirectProps } from './types';

interface LoginPanelProps {
readonly redirectURL?: string;
readonly handleLoginSelected: (_: string, _2?: string) => void;
}

interface LoginProviderItemProps extends LoginPanelProps {
interface LoginProviderItemProps extends LoginRedirectProps {
readonly provider: Gen3LoginProvider;
}

Expand Down Expand Up @@ -63,8 +59,8 @@ const LoginProviderSingleItem = ({ provider, handleLoginSelected, redirectURL }:
const LoginProvidersPanel = ({
handleLoginSelected,
redirectURL,
}: LoginPanelProps) => {
const { data, isSuccess } = useGetLoginProvidersQuery();;
}: LoginRedirectProps) => {
const { data, isSuccess } = useGetLoginProvidersQuery();

if (!isSuccess) {
return <LoadingOverlay visible={!isSuccess} />;
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/src/components/Login/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export interface LoginConfig extends Partial<Gen3LoginPanelConfig> {
topContent?: ReadonlyArray<TextContentProps>;
bottomContent?: ReadonlyArray<TextContentProps>;
}

export interface LoginRedirectProps {
readonly redirectURL?: string;
readonly handleLoginSelected: (_arg0: string, _arg1?: string) => void;
}
2 changes: 1 addition & 1 deletion packages/frontend/src/features/Query/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import GqlQueryEditor from './GqlQueryEditor';

export default GqlQueryEditor;
export { GqlQueryEditor };
1 change: 0 additions & 1 deletion packages/frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export * from './features/CohortBuilder';
export * from './utils/';

import { getNavPageLayoutPropsFromConfig } from './lib/common/staticProps';
import "@gen3/core";

// export Gen3 data UI standard pages
import Gen3Provider from './components/Providers/Gen3Provider';
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export { getCurrentUnixTimestamp, unixTimeToString };
export * from './authMapping';

export * from './focusStyle';

export * from './strings';
5 changes: 5 additions & 0 deletions packages/frontend/src/utils/strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const stripTrailingSlash = (str:string):string => {
return str.endsWith('/') ?
str.slice(0, -1) :
str;
};
4 changes: 2 additions & 2 deletions packages/sampleCommons/.env.development
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GEN3_COMMONS_NAME=brh
NEXT_PUBLIC_GEN3_API=https://localhost:3010
NEXT_PUBLIC_GEN3_API=https://brhstaging.data-commons.org/
NEXT_PUBLIC_GEN3_DOMAIN=https://localhost:3010
NEXT_PUBLIC_GEN3_MDS_API=https://brh.data-commons.org/mds
#NEXT_PUBLIC_GEN3_MDS_API=https://brh.data-commons.org/mds
#NEXT_PUBLIC_GEN3_GUPPY_API=https://localhost:3010/guppy
21 changes: 21 additions & 0 deletions packages/sampleCommons/src/pages/api/auth/setSessionToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { setCookie } from 'cookies-next';
import { decodeJwt, JWTPayload } from 'jose';

export default async function (req: NextApiRequest, res: NextApiResponse) {
const { access_token } = req.body;
const payload = decodeJwt(access_token) as JWTPayload;

if (!payload) {
res.status(400).json({ message: 'Invalid token' });
return;
}
console.log("payload: ", payload);
setCookie( 'access_token', access_token, {
req, res,
maxAge: payload && payload.exp && payload.iat ? payload.exp - payload.iat : 60 * 60 * 24 * 30,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
});
res.status(200).json({ message: 'Session token set' });
}

0 comments on commit bc87e1c

Please sign in to comment.