Skip to content

Commit

Permalink
Merge pull request #726 from hackforla/link-accounts
Browse files Browse the repository at this point in the history
Link accounts and stop account creation from sign in page
  • Loading branch information
paulespinosa authored Aug 22, 2024
2 parents 205d33d + 01678c3 commit f69d05b
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 44 deletions.
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 @@ -93,4 +95,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

0 comments on commit f69d05b

Please sign in to comment.