Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Link accounts and stop account creation from sign in page #726

Merged
merged 7 commits into from
Aug 22, 2024
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
154 changes: 127 additions & 27 deletions api/openapi_server/controllers/auth_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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']
Expand All @@ -369,7 +470,6 @@ def token(): # get code from body
'user': user
}


def current_session():
user_data = None
with DataAccessLayer.session() as db_session:
Expand Down
8 changes: 5 additions & 3 deletions api/openapi_server/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -79,4 +81,4 @@ components:
title: message
type: string
title: ApiResponse
type: object
type: object
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
28 changes: 28 additions & 0 deletions api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -14,23 +21,33 @@ export const redirectsByRole = {
};

interface UseAuthenticateWithOAuth {
query: (
arg: TokenRequest,
) => MutationActionCreatorResult<
MutationDefinition<
TokenRequest,
BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError>,
'Hosts',
TokenResponse,
'api'
>
>;
setErrorMessage: React.Dispatch<React.SetStateAction<string>>;
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,
})
Expand All @@ -51,7 +68,5 @@ export const useAuthenticateWithOAuth = ({
}
});
}
}, [location, setErrorMessage, getToken, dispatch, navigate, callbackUri]);

return {getTokenIsLoading};
}, [location, setErrorMessage, dispatch, navigate, callbackUri, query]);
};
18 changes: 15 additions & 3 deletions app/src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,22 @@ const authApi = api.injectEndpoints({
body: credentials,
}),
}),
getToken: build.mutation<TokenResponse, TokenRequest>({
googleSignUp: build.mutation<TokenResponse, TokenRequest>({
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<TokenResponse, TokenRequest>({
query: data => {
const {code, callbackUri} = data;
return {
url: `auth/google/sign_in?callback_uri=${callbackUri}`,
method: 'POST',
withCredentials: true,
body: {code},
Expand Down Expand Up @@ -212,7 +223,8 @@ export const {
useSignOutMutation,
useVerificationMutation,
useNewPasswordMutation,
useGetTokenMutation,
useGoogleSignUpMutation,
useGoogleSignInMutation,
useForgotPasswordMutation,
useConfirmForgotPasswordMutation,
useSessionMutation,
Expand Down
11 changes: 9 additions & 2 deletions app/src/views/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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',
});
Expand Down
Loading
Loading