Skip to content

Commit f69d05b

Browse files
authored
Merge pull request #726 from hackforla/link-accounts
Link accounts and stop account creation from sign in page
2 parents 205d33d + 01678c3 commit f69d05b

File tree

8 files changed

+227
-44
lines changed

8 files changed

+227
-44
lines changed

api/openapi_server/controllers/auth_controller.py

Lines changed: 127 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ def signout():
283283
# send response
284284
return response
285285

286-
def token(): # get code from body
286+
def google_sign_in():
287+
# get code from body
287288
code = request.get_json()['code']
288289
client_id = current_app.config['COGNITO_CLIENT_ID']
289290
client_secret = current_app.config['COGNITO_CLIENT_SECRET']
@@ -320,43 +321,143 @@ def token(): # get code from body
320321

321322
# create user object from user data
322323
user_attrs = get_user_attr(user_data)
323-
324-
# check if user exists in database
325-
user = None
326324

325+
# check if user exists in database
327326
with DataAccessLayer.session() as db_session:
328327
user_repo = UserRepository(db_session)
329328
signed_in_user = user_repo.get_user(user_attrs['email'])
330329
if(bool(signed_in_user) == True):
331330
user = user_schema.dump(signed_in_user)
331+
else:
332+
#if user does not exist in database, they haven't gone through sign up process, delete user from Cognito and return error
333+
try:
334+
decoded = jwt.decode(id_token, algorithms=["RS256"], options={"verify_signature": False})
335+
336+
current_app.logger.info('Deleting user from Cognito')
337+
response = current_app.boto_client.admin_delete_user(
338+
UserPoolId=current_app.config['COGNITO_USER_POOL_ID'],
339+
Username=decoded["cognito:username"]
340+
)
341+
current_app.logger.info('User deleted from Cognito')
342+
raise AuthError({
343+
'code': 'No user found',
344+
'message': 'No user found'
345+
}, 400)
346+
except botocore.exceptions.ClientError as e:
347+
current_app.logger.error('Failed to delete user from Cognito')
348+
code = e.response['Error']['Code']
349+
message = e.response['Error']['Message']
350+
raise AuthError({
351+
'code': code,
352+
'message': message
353+
}, 400)
354+
355+
# set refresh token cookie
356+
session['refresh_token'] = refresh_token
357+
session['username'] = user_attrs['email']
358+
session['id_token'] = id_token
359+
360+
361+
# return user data json
362+
return {
363+
'token': access_token,
364+
'user': user
365+
}
366+
367+
def google_sign_up():
368+
# get code from body
369+
code = request.get_json()['code']
370+
client_id = current_app.config['COGNITO_CLIENT_ID']
371+
client_secret = current_app.config['COGNITO_CLIENT_SECRET']
372+
callback_uri = request.args['callback_uri']
373+
374+
token_url = f"{cognito_client_url}/oauth2/token"
375+
auth = requests.auth.HTTPBasicAuth(client_id, client_secret)
376+
redirect_uri = f"{current_app.root_url}{callback_uri}"
377+
378+
params = {
379+
'grant_type': 'authorization_code',
380+
'client_id': client_id,
381+
'code': code,
382+
'redirect_uri': redirect_uri
383+
}
384+
385+
# get tokens from oauth2/token endpoint
386+
response = requests.post(token_url, auth=auth, data=params)
387+
388+
refresh_token = response.json().get('refresh_token')
389+
access_token = response.json().get('access_token')
390+
id_token = response.json().get('id_token')
332391

392+
# retrieve user data
393+
try:
394+
user_data = current_app.boto_client.get_user(AccessToken=access_token)
395+
except botocore.exceptions.ClientError as e:
396+
code = e.response['Error']['Code']
397+
message = e.response['Error']['Message']
398+
raise AuthError({
399+
"code": code,
400+
"message": message
401+
}, 401)
333402

334-
# If not, add user to database and get user object
335-
if(user is None):
336-
user_role = callback_uri.split('/')[2].capitalize()
337-
role = UserRole.COORDINATOR if user_role == 'Coordinator' else UserRole.HOST
403+
# create user object from user data
404+
user_attrs = get_user_attr(user_data)
405+
user_role = callback_uri.split('/')[2].capitalize()
338406

407+
role = None
408+
if user_role == 'Coordinator':
409+
role = UserRole.COORDINATOR
410+
411+
if user_role == 'Host':
412+
role = UserRole.HOST
413+
414+
# if role is None, delete user from Cognito and return error
415+
if role is None:
339416
try:
340-
with DataAccessLayer.session() as db_session:
341-
user_repo = UserRepository(db_session)
342-
user_repo.add_user(
343-
email=user_attrs['email'],
344-
role=role,
345-
firstName=user_attrs['first_name'],
346-
middleName=user_attrs.get('middle_name', ''),
347-
lastName=user_attrs.get('last_name', '')
348-
)
349-
except Exception as error:
350-
raise AuthError({"message": str(error)}, 400)
351-
417+
current_app.logger.info('Deleting user from Cognito')
418+
decoded = jwt.decode(id_token, algorithms=["RS256"], options={"verify_signature": False})
419+
420+
response = current_app.boto_client.admin_delete_user(
421+
UserPoolId=current_app.config['COGNITO_USER_POOL_ID'],
422+
Username=decoded["cognito:username"]
423+
)
424+
current_app.logger.info('User deleted from Cognito')
425+
raise AuthError({
426+
'code': 'invalid_role',
427+
'message': 'Invalid role. no role found provided'
428+
}, 400)
429+
except botocore.exceptions.ClientError as e:
430+
current_app.logger.error('Failed to delete user from Cognito')
431+
code = e.response['Error']['Code']
432+
message = e.response['Error']['Message']
433+
raise AuthError({
434+
'code': code,
435+
'message': message
436+
}, 400)
437+
438+
439+
440+
try:
352441
with DataAccessLayer.session() as db_session:
353442
user_repo = UserRepository(db_session)
354-
signed_in_user = user_repo.get_user(user_attrs['email'])
355-
if(bool(signed_in_user) == True):
356-
user = user_schema.dump(signed_in_user)
357-
else:
358-
raise AuthError({"message": "User not found in database"}, 400)
359-
443+
user_repo.add_user(
444+
email=user_attrs['email'],
445+
role=role,
446+
firstName=user_attrs['first_name'],
447+
middleName=user_attrs.get('middle_name', ''),
448+
lastName=user_attrs.get('last_name', '')
449+
)
450+
except Exception as error:
451+
raise AuthError({"message": str(error)}, 400)
452+
453+
with DataAccessLayer.session() as db_session:
454+
user_repo = UserRepository(db_session)
455+
signed_in_user = user_repo.get_user(user_attrs['email'])
456+
if(bool(signed_in_user) == True):
457+
user = user_schema.dump(signed_in_user)
458+
else:
459+
raise AuthError({"message": "User not found in database"}, 400)
460+
360461
# set refresh token cookie
361462
session['refresh_token'] = refresh_token
362463
session['username'] = user_attrs['email']
@@ -369,7 +470,6 @@ def token(): # get code from body
369470
'user': user
370471
}
371472

372-
373473
def current_session():
374474
user_data = None
375475
with DataAccessLayer.session() as db_session:

api/openapi_server/openapi/openapi.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ paths:
2727
$ref: "./paths/auth/authConfirm.yaml"
2828
/auth/signout:
2929
$ref: "./paths/auth/authSignout.yaml"
30-
/auth/token:
31-
$ref: "./paths/auth/authToken.yaml"
3230
/auth/session:
3331
$ref: "./paths/auth/authSession.yaml"
3432
/auth/refresh:
@@ -43,6 +41,10 @@ paths:
4341
$ref: "./paths/auth/authPrivate.yaml"
4442
/auth/google:
4543
$ref: "./paths/auth/authGoogle.yaml"
44+
/auth/google/sign_up:
45+
$ref: "./paths/auth/authGoogleSignUp.yaml"
46+
/auth/google/sign_in:
47+
$ref: "./paths/auth/authGoogleSignIn.yaml"
4648
/auth/new_password:
4749
$ref: "./paths/auth/authNewPassword.yaml"
4850
/auth/invite:
@@ -93,4 +95,4 @@ components:
9395
title: message
9496
type: string
9597
title: ApiResponse
96-
type: object
98+
type: object

api/openapi_server/openapi/paths/auth/authToken.yaml renamed to api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
post:
22
description: Sign in user from OAuth Provider
3-
operationId: token
3+
operationId: google_sign_in
4+
requestBody:
5+
required: true
6+
content:
7+
application/json:
8+
schema:
9+
type: object
10+
properties:
11+
code:
12+
type: string
13+
parameters:
14+
- in: query
15+
name: callback_uri
16+
schema:
17+
type: string
18+
required: true
419
responses:
520
"200":
621
content:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
post:
2+
description: Sign in user from OAuth Provider
3+
operationId: google_sign_up
4+
requestBody:
5+
required: true
6+
content:
7+
application/json:
8+
schema:
9+
type: object
10+
properties:
11+
code:
12+
type: string
13+
parameters:
14+
- in: query
15+
name: callback_uri
16+
schema:
17+
type: string
18+
required: true
19+
responses:
20+
"200":
21+
content:
22+
application/json:
23+
schema:
24+
$ref: "../../openapi.yaml#/components/schemas/ApiResponse"
25+
description: successful operation
26+
tags:
27+
- auth
28+
x-openapi-router-controller: openapi_server.controllers.auth_controller

app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import React from 'react';
22
import {setCredentials} from '../../../app/authSlice';
33
import {isFetchBaseQueryError, isErrorWithMessage} from '../../../app/helpers';
4-
import {useGetTokenMutation} from '../../../services/auth';
4+
import {TokenRequest, TokenResponse} from '../../../services/auth';
55
import {useNavigate} from 'react-router-dom';
66
import {useAppDispatch} from '../../../app/hooks/store';
7+
import {
8+
MutationActionCreatorResult,
9+
MutationDefinition,
10+
BaseQueryFn,
11+
FetchArgs,
12+
FetchBaseQueryError,
13+
} from '@reduxjs/toolkit/query';
714

815
// TODO: Maybe store this in a more global location? with routes?
916
export const redirectsByRole = {
@@ -14,23 +21,33 @@ export const redirectsByRole = {
1421
};
1522

1623
interface UseAuthenticateWithOAuth {
24+
query: (
25+
arg: TokenRequest,
26+
) => MutationActionCreatorResult<
27+
MutationDefinition<
28+
TokenRequest,
29+
BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError>,
30+
'Hosts',
31+
TokenResponse,
32+
'api'
33+
>
34+
>;
1735
setErrorMessage: React.Dispatch<React.SetStateAction<string>>;
1836
callbackUri: string;
1937
}
2038

2139
export const useAuthenticateWithOAuth = ({
40+
query,
2241
setErrorMessage,
2342
callbackUri,
2443
}: UseAuthenticateWithOAuth) => {
2544
const navigate = useNavigate();
2645
const dispatch = useAppDispatch();
2746

28-
const [getToken, {isLoading: getTokenIsLoading}] = useGetTokenMutation();
29-
3047
React.useEffect(() => {
3148
if (location.search.includes('code')) {
3249
const code = location.search.split('?code=')[1];
33-
getToken({
50+
query({
3451
code,
3552
callbackUri,
3653
})
@@ -51,7 +68,5 @@ export const useAuthenticateWithOAuth = ({
5168
}
5269
});
5370
}
54-
}, [location, setErrorMessage, getToken, dispatch, navigate, callbackUri]);
55-
56-
return {getTokenIsLoading};
71+
}, [location, setErrorMessage, dispatch, navigate, callbackUri, query]);
5772
};

app/src/services/auth.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,22 @@ const authApi = api.injectEndpoints({
119119
body: credentials,
120120
}),
121121
}),
122-
getToken: build.mutation<TokenResponse, TokenRequest>({
122+
googleSignUp: build.mutation<TokenResponse, TokenRequest>({
123123
query: data => {
124124
const {code, callbackUri} = data;
125125
return {
126-
url: `auth/token?callback_uri=${callbackUri}`,
126+
url: `auth/google/sign_up?callback_uri=${callbackUri}`,
127+
method: 'POST',
128+
withCredentials: true,
129+
body: {code},
130+
};
131+
},
132+
}),
133+
googleSignIn: build.mutation<TokenResponse, TokenRequest>({
134+
query: data => {
135+
const {code, callbackUri} = data;
136+
return {
137+
url: `auth/google/sign_in?callback_uri=${callbackUri}`,
127138
method: 'POST',
128139
withCredentials: true,
129140
body: {code},
@@ -212,7 +223,8 @@ export const {
212223
useSignOutMutation,
213224
useVerificationMutation,
214225
useNewPasswordMutation,
215-
useGetTokenMutation,
226+
useGoogleSignUpMutation,
227+
useGoogleSignInMutation,
216228
useForgotPasswordMutation,
217229
useConfirmForgotPasswordMutation,
218230
useSessionMutation,

app/src/views/SignIn.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import CloseIcon from '@mui/icons-material/Close';
1313
import {setCredentials} from '../app/authSlice';
1414
import {useAppDispatch} from '../app/hooks/store';
1515
import {SignInForm} from '../components/authentication/SignInForm';
16-
import {SignInRequest, useSignInMutation} from '../services/auth';
16+
import {
17+
SignInRequest,
18+
useGoogleSignInMutation,
19+
useSignInMutation,
20+
} from '../services/auth';
1721
import {isFetchBaseQueryError, isErrorWithMessage} from '../app/helpers';
1822
import {FormContainer} from '../components/authentication';
1923
import {
@@ -30,11 +34,14 @@ export const SignIn = () => {
3034
const navigate = useNavigate();
3135
const dispatch = useAppDispatch();
3236
const [signIn, {isLoading: signInIsLoading}] = useSignInMutation();
37+
const [googleSignIn, {isLoading: getTokenIsLoading}] =
38+
useGoogleSignInMutation();
3339
// const locationState = location.state as LocationState;
3440

3541
// Save location from which user was redirected to login page
3642
// const from = locationState?.from?.pathname || '/';
37-
const {getTokenIsLoading} = useAuthenticateWithOAuth({
43+
useAuthenticateWithOAuth({
44+
query: googleSignIn,
3845
setErrorMessage,
3946
callbackUri: '/signin',
4047
});

0 commit comments

Comments
 (0)