diff --git a/api/openapi_server/controllers/auth_controller.py b/api/openapi_server/controllers/auth_controller.py index 7812d399..6ba71acb 100644 --- a/api/openapi_server/controllers/auth_controller.py +++ b/api/openapi_server/controllers/auth_controller.py @@ -283,7 +283,8 @@ def signout(): # send response return response -def token(): # get code from body +def google_sign_in(): + # get code from body code = request.get_json()['code'] client_id = current_app.config['COGNITO_CLIENT_ID'] client_secret = current_app.config['COGNITO_CLIENT_SECRET'] @@ -320,43 +321,143 @@ def token(): # get code from body # create user object from user data user_attrs = get_user_attr(user_data) - - # check if user exists in database - user = None + # check if user exists in database with DataAccessLayer.session() as db_session: user_repo = UserRepository(db_session) signed_in_user = user_repo.get_user(user_attrs['email']) if(bool(signed_in_user) == True): user = user_schema.dump(signed_in_user) + else: + #if user does not exist in database, they haven't gone through sign up process, delete user from Cognito and return error + try: + decoded = jwt.decode(id_token, algorithms=["RS256"], options={"verify_signature": False}) + + current_app.logger.info('Deleting user from Cognito') + response = current_app.boto_client.admin_delete_user( + UserPoolId=current_app.config['COGNITO_USER_POOL_ID'], + Username=decoded["cognito:username"] + ) + current_app.logger.info('User deleted from Cognito') + raise AuthError({ + 'code': 'No user found', + 'message': 'No user found' + }, 400) + except botocore.exceptions.ClientError as e: + current_app.logger.error('Failed to delete user from Cognito') + code = e.response['Error']['Code'] + message = e.response['Error']['Message'] + raise AuthError({ + 'code': code, + 'message': message + }, 400) + + # set refresh token cookie + session['refresh_token'] = refresh_token + session['username'] = user_attrs['email'] + session['id_token'] = id_token + + + # return user data json + return { + 'token': access_token, + 'user': user + } + +def google_sign_up(): + # get code from body + code = request.get_json()['code'] + client_id = current_app.config['COGNITO_CLIENT_ID'] + client_secret = current_app.config['COGNITO_CLIENT_SECRET'] + callback_uri = request.args['callback_uri'] + + token_url = f"{cognito_client_url}/oauth2/token" + auth = requests.auth.HTTPBasicAuth(client_id, client_secret) + redirect_uri = f"{current_app.root_url}{callback_uri}" + + params = { + 'grant_type': 'authorization_code', + 'client_id': client_id, + 'code': code, + 'redirect_uri': redirect_uri + } + + # get tokens from oauth2/token endpoint + response = requests.post(token_url, auth=auth, data=params) + + refresh_token = response.json().get('refresh_token') + access_token = response.json().get('access_token') + id_token = response.json().get('id_token') + # retrieve user data + try: + user_data = current_app.boto_client.get_user(AccessToken=access_token) + except botocore.exceptions.ClientError as e: + code = e.response['Error']['Code'] + message = e.response['Error']['Message'] + raise AuthError({ + "code": code, + "message": message + }, 401) - # If not, add user to database and get user object - if(user is None): - user_role = callback_uri.split('/')[2].capitalize() - role = UserRole.COORDINATOR if user_role == 'Coordinator' else UserRole.HOST + # create user object from user data + user_attrs = get_user_attr(user_data) + user_role = callback_uri.split('/')[2].capitalize() + role = None + if user_role == 'Coordinator': + role = UserRole.COORDINATOR + + if user_role == 'Host': + role = UserRole.HOST + + # if role is None, delete user from Cognito and return error + if role is None: try: - with DataAccessLayer.session() as db_session: - user_repo = UserRepository(db_session) - user_repo.add_user( - email=user_attrs['email'], - role=role, - firstName=user_attrs['first_name'], - middleName=user_attrs.get('middle_name', ''), - lastName=user_attrs.get('last_name', '') - ) - except Exception as error: - raise AuthError({"message": str(error)}, 400) - + current_app.logger.info('Deleting user from Cognito') + decoded = jwt.decode(id_token, algorithms=["RS256"], options={"verify_signature": False}) + + response = current_app.boto_client.admin_delete_user( + UserPoolId=current_app.config['COGNITO_USER_POOL_ID'], + Username=decoded["cognito:username"] + ) + current_app.logger.info('User deleted from Cognito') + raise AuthError({ + 'code': 'invalid_role', + 'message': 'Invalid role. no role found provided' + }, 400) + except botocore.exceptions.ClientError as e: + current_app.logger.error('Failed to delete user from Cognito') + code = e.response['Error']['Code'] + message = e.response['Error']['Message'] + raise AuthError({ + 'code': code, + 'message': message + }, 400) + + + + try: with DataAccessLayer.session() as db_session: user_repo = UserRepository(db_session) - signed_in_user = user_repo.get_user(user_attrs['email']) - if(bool(signed_in_user) == True): - user = user_schema.dump(signed_in_user) - else: - raise AuthError({"message": "User not found in database"}, 400) - + user_repo.add_user( + email=user_attrs['email'], + role=role, + firstName=user_attrs['first_name'], + middleName=user_attrs.get('middle_name', ''), + lastName=user_attrs.get('last_name', '') + ) + except Exception as error: + raise AuthError({"message": str(error)}, 400) + + with DataAccessLayer.session() as db_session: + user_repo = UserRepository(db_session) + signed_in_user = user_repo.get_user(user_attrs['email']) + if(bool(signed_in_user) == True): + user = user_schema.dump(signed_in_user) + else: + raise AuthError({"message": "User not found in database"}, 400) + # set refresh token cookie session['refresh_token'] = refresh_token session['username'] = user_attrs['email'] @@ -369,7 +470,6 @@ def token(): # get code from body 'user': user } - def current_session(): user_data = None with DataAccessLayer.session() as db_session: diff --git a/api/openapi_server/openapi/openapi.yaml b/api/openapi_server/openapi/openapi.yaml index a7ac88cf..6b4dfdf7 100644 --- a/api/openapi_server/openapi/openapi.yaml +++ b/api/openapi_server/openapi/openapi.yaml @@ -27,8 +27,6 @@ paths: $ref: "./paths/auth/authConfirm.yaml" /auth/signout: $ref: "./paths/auth/authSignout.yaml" - /auth/token: - $ref: "./paths/auth/authToken.yaml" /auth/session: $ref: "./paths/auth/authSession.yaml" /auth/refresh: @@ -43,6 +41,10 @@ paths: $ref: "./paths/auth/authPrivate.yaml" /auth/google: $ref: "./paths/auth/authGoogle.yaml" + /auth/google/sign_up: + $ref: "./paths/auth/authGoogleSignUp.yaml" + /auth/google/sign_in: + $ref: "./paths/auth/authGoogleSignIn.yaml" /auth/new_password: $ref: "./paths/auth/authNewPassword.yaml" /auth/invite: @@ -79,4 +81,4 @@ components: title: message type: string title: ApiResponse - type: object \ No newline at end of file + type: object diff --git a/api/openapi_server/openapi/paths/auth/authToken.yaml b/api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml similarity index 51% rename from api/openapi_server/openapi/paths/auth/authToken.yaml rename to api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml index f9ab6e61..0c9a7b0d 100644 --- a/api/openapi_server/openapi/paths/auth/authToken.yaml +++ b/api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml @@ -1,6 +1,21 @@ post: description: Sign in user from OAuth Provider - operationId: token + operationId: google_sign_in + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + code: + type: string + parameters: + - in: query + name: callback_uri + schema: + type: string + required: true responses: "200": content: diff --git a/api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml b/api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml new file mode 100644 index 00000000..8e1db7d4 --- /dev/null +++ b/api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml @@ -0,0 +1,28 @@ +post: + description: Sign in user from OAuth Provider + operationId: google_sign_up + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + code: + type: string + parameters: + - in: query + name: callback_uri + schema: + type: string + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "../../openapi.yaml#/components/schemas/ApiResponse" + description: successful operation + tags: + - auth + x-openapi-router-controller: openapi_server.controllers.auth_controller diff --git a/app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts b/app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts index d687ae27..393e3be9 100644 --- a/app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts +++ b/app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts @@ -1,9 +1,16 @@ import React from 'react'; import {setCredentials} from '../../../app/authSlice'; import {isFetchBaseQueryError, isErrorWithMessage} from '../../../app/helpers'; -import {useGetTokenMutation} from '../../../services/auth'; +import {TokenRequest, TokenResponse} from '../../../services/auth'; import {useNavigate} from 'react-router-dom'; import {useAppDispatch} from '../../../app/hooks/store'; +import { + MutationActionCreatorResult, + MutationDefinition, + BaseQueryFn, + FetchArgs, + FetchBaseQueryError, +} from '@reduxjs/toolkit/query'; // TODO: Maybe store this in a more global location? with routes? export const redirectsByRole = { @@ -14,23 +21,33 @@ export const redirectsByRole = { }; interface UseAuthenticateWithOAuth { + query: ( + arg: TokenRequest, + ) => MutationActionCreatorResult< + MutationDefinition< + TokenRequest, + BaseQueryFn, + 'Hosts', + TokenResponse, + 'api' + > + >; setErrorMessage: React.Dispatch>; callbackUri: string; } export const useAuthenticateWithOAuth = ({ + query, setErrorMessage, callbackUri, }: UseAuthenticateWithOAuth) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); - const [getToken, {isLoading: getTokenIsLoading}] = useGetTokenMutation(); - React.useEffect(() => { if (location.search.includes('code')) { const code = location.search.split('?code=')[1]; - getToken({ + query({ code, callbackUri, }) @@ -51,7 +68,5 @@ export const useAuthenticateWithOAuth = ({ } }); } - }, [location, setErrorMessage, getToken, dispatch, navigate, callbackUri]); - - return {getTokenIsLoading}; + }, [location, setErrorMessage, dispatch, navigate, callbackUri, query]); }; diff --git a/app/src/services/auth.ts b/app/src/services/auth.ts index a99c565b..3d0b453c 100644 --- a/app/src/services/auth.ts +++ b/app/src/services/auth.ts @@ -119,11 +119,22 @@ const authApi = api.injectEndpoints({ body: credentials, }), }), - getToken: build.mutation({ + googleSignUp: build.mutation({ query: data => { const {code, callbackUri} = data; return { - url: `auth/token?callback_uri=${callbackUri}`, + url: `auth/google/sign_up?callback_uri=${callbackUri}`, + method: 'POST', + withCredentials: true, + body: {code}, + }; + }, + }), + googleSignIn: build.mutation({ + query: data => { + const {code, callbackUri} = data; + return { + url: `auth/google/sign_in?callback_uri=${callbackUri}`, method: 'POST', withCredentials: true, body: {code}, @@ -212,7 +223,8 @@ export const { useSignOutMutation, useVerificationMutation, useNewPasswordMutation, - useGetTokenMutation, + useGoogleSignUpMutation, + useGoogleSignInMutation, useForgotPasswordMutation, useConfirmForgotPasswordMutation, useSessionMutation, diff --git a/app/src/views/SignIn.tsx b/app/src/views/SignIn.tsx index 4a380979..83eccdbf 100644 --- a/app/src/views/SignIn.tsx +++ b/app/src/views/SignIn.tsx @@ -13,7 +13,11 @@ import CloseIcon from '@mui/icons-material/Close'; import {setCredentials} from '../app/authSlice'; import {useAppDispatch} from '../app/hooks/store'; import {SignInForm} from '../components/authentication/SignInForm'; -import {SignInRequest, useSignInMutation} from '../services/auth'; +import { + SignInRequest, + useGoogleSignInMutation, + useSignInMutation, +} from '../services/auth'; import {isFetchBaseQueryError, isErrorWithMessage} from '../app/helpers'; import {FormContainer} from '../components/authentication'; import { @@ -30,11 +34,14 @@ export const SignIn = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const [signIn, {isLoading: signInIsLoading}] = useSignInMutation(); + const [googleSignIn, {isLoading: getTokenIsLoading}] = + useGoogleSignInMutation(); // const locationState = location.state as LocationState; // Save location from which user was redirected to login page // const from = locationState?.from?.pathname || '/'; - const {getTokenIsLoading} = useAuthenticateWithOAuth({ + useAuthenticateWithOAuth({ + query: googleSignIn, setErrorMessage, callbackUri: '/signin', }); diff --git a/app/src/views/SignUp.tsx b/app/src/views/SignUp.tsx index 8cee004a..dc632032 100644 --- a/app/src/views/SignUp.tsx +++ b/app/src/views/SignUp.tsx @@ -16,6 +16,7 @@ import { SignUpCoordinatorRequest, useSignUpHostMutation, useSignUpCoordinatorMutation, + useGoogleSignUpMutation, } from '../services/auth'; import {isErrorWithMessage, isFetchBaseQueryError} from '../app/helpers'; import {FormContainer} from '../components/authentication'; @@ -30,6 +31,8 @@ export const SignUp = () => { useSignUpHostMutation(); const [signUpCoordinator, {isLoading: signUpCoordinatorIsLoading}] = useSignUpCoordinatorMutation(); + const [googleSignUp, {isLoading: getTokenIsLoading}] = + useGoogleSignUpMutation(); // get type from params // const locationState = location.state as LocationState; @@ -37,7 +40,8 @@ export const SignUp = () => { // const from = locationState?.from?.pathname || '/'; const callbackUri = `/signup/${type}`; - const {getTokenIsLoading} = useAuthenticateWithOAuth({ + useAuthenticateWithOAuth({ + query: googleSignUp, setErrorMessage, callbackUri, });