From 45e435dbb4c43cb890eb360413784d0b2e331bc5 Mon Sep 17 00:00:00 2001 From: Shravya Ramesh <37447613+shravyaramesh@users.noreply.github.com> Date: Wed, 7 Oct 2020 23:06:32 -0700 Subject: [TMA 68] Frontend Token Security (#43) * frontend tma-68 token security * removed: try catch while storing token to async, unnecessary console.log * login/registration exception handling and relocation * Modified promises, applied fetch restriction --- src/components/search/SearchResult.tsx | 16 ++++++- src/routes/authentication/AuthProvider.tsx | 66 ++++++++++++++++++++++------ src/screens/onboarding/Login.tsx | 24 ++++++++-- src/screens/onboarding/ProfileOnboarding.tsx | 4 ++ src/screens/onboarding/RegistrationThree.tsx | 14 ++++-- src/screens/onboarding/RegistrationTwo.tsx | 1 + src/screens/search/SearchScreen.tsx | 14 ++++++ 7 files changed, 116 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/components/search/SearchResult.tsx b/src/components/search/SearchResult.tsx index e65be1f4..952f08f7 100644 --- a/src/components/search/SearchResult.tsx +++ b/src/components/search/SearchResult.tsx @@ -11,6 +11,11 @@ import { import RNFetchBlob from 'rn-fetch-blob'; import AsyncStorage from '@react-native-community/async-storage'; import {AVATAR_PHOTO_ENDPOINT} from '../../constants'; +import {UserType} from '../../types'; +const NO_USER: UserType = { + userId: '', + username: '', +}; interface SearchResultProps extends ViewProps { profilePreview: ProfilePreviewType; @@ -20,15 +25,22 @@ const SearchResult: React.FC = ({ style, }) => { const [avatarURI, setAvatarURI] = useState(null); - + const [user, setUser] = useState(NO_USER); useEffect(() => { let mounted = true; const loadAvatar = async () => { try { + const token = await AsyncStorage.getItem('token'); + if (!token) { + setUser(NO_USER); + return; + } const response = await RNFetchBlob.config({ fileCache: true, appendExt: 'jpg', - }).fetch('GET', AVATAR_PHOTO_ENDPOINT + `${id}`); + }).fetch('GET', AVATAR_PHOTO_ENDPOINT + `${id}`, { + Authorization: 'Token ' + token, + }); const status = response.info().status; if (status === 200) { if (mounted) { diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx index 589cb051..e5956eb2 100644 --- a/src/routes/authentication/AuthProvider.tsx +++ b/src/routes/authentication/AuthProvider.tsx @@ -14,6 +14,7 @@ import { COVER_PHOTO_ENDPOINT, GET_IG_POSTS_ENDPOINT, } from '../../constants'; +import {Alert} from 'react-native'; interface AuthContextProps { user: UserType; @@ -57,16 +58,18 @@ const AuthProvider: React.FC = ({children}) => { const [recentSearches, setRecentSearches] = useState< Array >([]); - const {userId} = user; useEffect(() => { if (!userId) { return; } - const loadProfileInfo = async () => { + const loadProfileInfo = async (token: string) => { try { const response = await fetch(PROFILE_INFO_ENDPOINT + `${userId}/`, { method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, }); const status = response.status; if (status === 200) { @@ -75,15 +78,20 @@ const AuthProvider: React.FC = ({children}) => { setProfile({name, biography, website}); } } catch (error) { - console.log(error); + Alert.alert( + 'Something went wrong! 😭', + "Would you believe me if I told you that I don't know what happened?", + ); } }; - const loadAvatar = async () => { + const loadAvatar = async (token: string) => { try { const response = await RNFetchBlob.config({ fileCache: true, appendExt: 'jpg', - }).fetch('GET', AVATAR_PHOTO_ENDPOINT + `${userId}/`); + }).fetch('GET', AVATAR_PHOTO_ENDPOINT + `${userId}/`, { + Authorization: 'Token ' + token, + }); const status = response.info().status; if (status === 200) { setAvatar(response.path()); @@ -94,12 +102,14 @@ const AuthProvider: React.FC = ({children}) => { console.log(error); } }; - const loadCover = async () => { + const loadCover = async (token: string) => { try { let response = await RNFetchBlob.config({ fileCache: true, appendExt: 'jpg', - }).fetch('GET', COVER_PHOTO_ENDPOINT + `${userId}/`); + }).fetch('GET', COVER_PHOTO_ENDPOINT + `${userId}/`, { + Authorization: 'Token ' + token, + }); const status = response.info().status; if (status === 200) { setCover(response.path()); @@ -110,10 +120,13 @@ const AuthProvider: React.FC = ({children}) => { console.log(error); } }; - const loadInstaPosts = async () => { + const loadInstaPosts = async (token: string) => { try { const response = await fetch(GET_IG_POSTS_ENDPOINT + `${userId}/`, { method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, }); const status = response.status; if (status === 200) { @@ -124,6 +137,10 @@ const AuthProvider: React.FC = ({children}) => { } } catch (error) { console.log(error); + Alert.alert( + 'Something went wrong! 😭', + "Would you believe me if I told you that I don't know what happened?", + ); } }; const loadRecentlySearchedUsers = async () => { @@ -136,11 +153,24 @@ const AuthProvider: React.FC = ({children}) => { console.log(e); } }; - loadProfileInfo(); - loadAvatar(); - loadCover(); - loadInstaPosts(); - loadRecentlySearchedUsers(); + + const loadData = async () => { + try { + const token = await AsyncStorage.getItem('token'); + if (!token) { + setUser(NO_USER); + return; + } + loadProfileInfo(token); + loadAvatar(token); + loadCover(token); + loadInstaPosts(token); + loadRecentlySearchedUsers(); + } catch (err) { + console.log(err); + } + }; + loadData(); }, [userId]); return ( @@ -155,7 +185,15 @@ const AuthProvider: React.FC = ({children}) => { setUser({...user, userId: id, username}); }, logout: () => { - setUser(NO_USER); + try { + new Promise(() => { + AsyncStorage.removeItem('token'); + }).then(() => { + setUser(NO_USER); + }); + } catch (err) { + console.log(err); + } }, recentSearches, }}> diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 5c569ec3..c0dc14b7 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react'; +import React, {useRef, useState} from 'react'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import { @@ -17,6 +17,8 @@ import {OnboardingStackParams} from '../../routes/onboarding'; import {AuthContext} from '../../routes/authentication'; import {Background, TaggInput, SubmitButton} from '../../components'; import {usernameRegex, LOGIN_ENDPOINT} from '../../constants'; +import AsyncStorage from '@react-native-community/async-storage'; +import {UserType} from '../../types'; type VerificationScreenRouteProp = RouteProp; type VerificationScreenNavigationProp = StackNavigationProp< @@ -34,6 +36,12 @@ interface LoginProps { const Login: React.FC = ({navigation}: LoginProps) => { // ref for focusing on input fields const inputRef = useRef(); + + const NO_USER: UserType = { + userId: '', + username: '', + }; + // login form state const [form, setForm] = React.useState({ username: '', @@ -41,10 +49,11 @@ const Login: React.FC = ({navigation}: LoginProps) => { isValidUser: false, isValidPassword: false, attemptedSubmit: false, + token: '', }); // determines if user is logged in const {login} = React.useContext(AuthContext); - + const [user, setUser] = useState(NO_USER); /** * Updates the state of username. Also verifies the input of the username field by ensuring proper length and appropriate characters. */ @@ -101,6 +110,7 @@ const Login: React.FC = ({navigation}: LoginProps) => { /** * Handler for the Let's Start button or the Go button on the keyboard. Makes a POST request to the Django login API and presents Alerts based on the status codes that the backend returns. + * Stores token received in the response, into client's AsynStorage */ const handleLogin = async () => { if (!form.attemptedSubmit) { @@ -122,8 +132,16 @@ const Login: React.FC = ({navigation}: LoginProps) => { let statusCode = response.status; let data = await response.json(); + if (statusCode === 200) { - login(data.UserID, username); + //Stores token received in the response into client's AsynStorage + try { + await AsyncStorage.setItem('token', data.token); + login(data.UserID, username); + } catch (err) { + setUser(NO_USER); + Alert.alert('Auth token storage failed', 'Please login again!'); + } } else if (statusCode === 401) { Alert.alert( 'Login failed 😔', diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx index 814cd82e..506d5f63 100644 --- a/src/screens/onboarding/ProfileOnboarding.tsx +++ b/src/screens/onboarding/ProfileOnboarding.tsx @@ -27,6 +27,7 @@ import { genderRegex, } from '../../constants'; import moment from 'moment'; +import AsyncStorage from '@react-native-community/async-storage'; type ProfileOnboardingScreenRouteProp = RouteProp< OnboardingStackParams, @@ -59,6 +60,7 @@ const ProfileOnboarding: React.FC = ({route}) => { isValidBio: true, isValidGender: true, attemptedSubmit: false, + token: '', }); const [customGender, setCustomGender] = React.useState(); @@ -314,10 +316,12 @@ const ProfileOnboarding: React.FC = ({route}) => { const endpoint = EDIT_PROFILE_ENDPOINT + `${userId}/`; try { + const token = await AsyncStorage.getItem('token'); let response = await fetch(endpoint, { method: 'PATCH', headers: { 'Content-Type': 'multipart/form-data', + Authorization: 'Token ' + token, }, body: request, }); diff --git a/src/screens/onboarding/RegistrationThree.tsx b/src/screens/onboarding/RegistrationThree.tsx index f8daaf71..5b8f52b3 100644 --- a/src/screens/onboarding/RegistrationThree.tsx +++ b/src/screens/onboarding/RegistrationThree.tsx @@ -28,6 +28,7 @@ import { Background, } from '../../components'; import {passwordRegex, usernameRegex, REGISTER_ENDPOINT} from '../../constants'; +import AsyncStorage from '@react-native-community/async-storage'; type RegistrationScreenThreeRouteProp = RouteProp< OnboardingStackParams, @@ -170,10 +171,15 @@ const RegistrationThree: React.FC = ({ let data = await registerResponse.json(); const userId: string = data.UserID; if (statusCode === 201) { - navigation.navigate('Checkpoint', { - userId: userId, - username: form.username, - }); + try { + await AsyncStorage.setItem('token', data.token); + navigation.navigate('Checkpoint', { + userId: userId, + username: form.username, + }); + } catch (err) { + console.log(err); + } } else if (statusCode === 409) { Alert.alert('Registration failed 😔', `${data}`); } else { diff --git a/src/screens/onboarding/RegistrationTwo.tsx b/src/screens/onboarding/RegistrationTwo.tsx index 0ce4f410..d28fb197 100644 --- a/src/screens/onboarding/RegistrationTwo.tsx +++ b/src/screens/onboarding/RegistrationTwo.tsx @@ -70,6 +70,7 @@ const RegistrationTwo: React.FC = ({ isValidFname: false, isValidLname: false, attemptedSubmit: false, + token: '', }); /* diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 2a2a5a4a..da83ddef 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -16,6 +16,11 @@ import AsyncStorage from '@react-native-community/async-storage'; import {ProfilePreviewType} from '../../types'; import {SEARCH_ENDPOINT} from '../../constants'; import {AuthContext} from '../../routes/authentication'; +import {UserType} from '../../types'; +const NO_USER: UserType = { + userId: '', + username: '', +}; /** * Search Screen for user recommendations and a search @@ -31,6 +36,7 @@ const SearchScreen: React.FC = () => { ); const [searching, setSearching] = useState(false); const top = Animated.useValue(-SCREEN_HEIGHT); + const [user, setUser] = useState(NO_USER); useEffect(() => { if (query.length < 3) { setResults([]); @@ -38,8 +44,16 @@ const SearchScreen: React.FC = () => { } const loadResults = async (q: string) => { try { + const token = await AsyncStorage.getItem('token'); + if (!token) { + setUser(NO_USER); + return; + } const response = await fetch(`${SEARCH_ENDPOINT}?query=${q}`, { method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, }); const status = response.status; if (status === 200) { -- cgit v1.2.3-70-g09d2