Skip to content

Commit

Permalink
Validate token expires (#353)
Browse files Browse the repository at this point in the history
* Add logout button
* Use constants for property save/load
* Correctly config type checking
* Config biome
* Validate token expire time
  • Loading branch information
NigelBreslaw authored Feb 12, 2024
1 parent 46330a3 commit 7ea3c81
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 40 deletions.
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"indentWidth": 2,
"indentStyle": "space",
"lineWidth": 120,
"ignore": ["ios/**", "android/**", "node_modules/**", "app.json", "eas.json"]
"ignore": ["ios/**", "android/**", "node_modules/**", ".expo/**", "app.json", "eas.json"]
},
"linter": {
"enabled": true,
Expand Down
3 changes: 2 additions & 1 deletion native_gg/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { Button, StyleSheet, Text, View } from "react-native";
import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
import { useEffect, useReducer } from "react";
Expand Down Expand Up @@ -67,6 +67,7 @@ export default function App() {
<Text style={{ fontSize: 22, marginTop: 15, color: "#150f63" }}>
Authenticated: <Text style={{ fontWeight: "bold" }}>{state.authenticated ? "True" : "False"}</Text>
</Text>
<Button title="Logout" onPress={() => AuthService.logoutCurrentUser()} />
<StatusBar style="auto" />
</View>
);
Expand Down
46 changes: 22 additions & 24 deletions native_gg/src/authentication/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,11 @@ import { randomUUID } from "expo-crypto";
import { parse } from "expo-linking";
import * as WebBrowser from "expo-web-browser";
import * as v from "valibot";
import { getAccessToken, getRefreshToken } from "./Utilities.ts";
import { clientID, redirectURL } from "../constants/env.ts";
import { Store } from "../constants/storage.ts";
import { AppAction } from "../state/Actions.ts";

const refreshTokenSchema = v.object({
access_token: v.string(),
expires_in: v.number(),
membership_id: v.string(),
refresh_expires_in: v.number(),
refresh_token: v.string(),
time_stamp: v.optional(v.string([v.isoTimestamp()])),
token_type: v.string(),
});

type RefreshToken = v.Output<typeof refreshTokenSchema>;
import { RefreshToken, refreshTokenSchema } from "./Types.ts";
import { getAccessToken, getRefreshToken } from "./Utilities.ts";

class AuthService {
private static instance: AuthService;
Expand Down Expand Up @@ -57,7 +47,7 @@ class AuthService {
init(): Promise<boolean> {
return new Promise((resolve, reject) => {
// Is there a current user?
AsyncStorage.getItem("current_user_id")
AsyncStorage.getItem(Store.current_user_ID)
.then((current_user) => {
if (current_user === null) {
return reject(false);
Expand All @@ -66,7 +56,7 @@ class AuthService {
console.log("user!", this.currentUserID);

// Then is there an auth token?
AsyncStorage.getItem(`${this.currentUserID}_refresh_token`)
AsyncStorage.getItem(`${this.currentUserID}${Store._refresh_token}`)
.then((token) => {
console.log("token!", token !== null);

Expand All @@ -76,8 +66,8 @@ class AuthService {
this.setAuthToken(validatedToken);
return resolve(true);
} catch (error) {
console.error(error);
return resolve(false);
console.log(error);
return reject(false);
}
})
.catch((e) => {
Expand Down Expand Up @@ -120,7 +110,7 @@ class AuthService {
}
}

setAuthToken(token: RefreshToken) {
setAuthToken(token: RefreshToken | null) {
this.authToken = token;
if (this.dispatch) {
this.dispatch({ type: "setAuthenticated", payload: this.isAuthenticated() });
Expand Down Expand Up @@ -161,16 +151,16 @@ class AuthService {
const validatedToken = v.parse(refreshTokenSchema, initialJSONToken);
this.setCurrentUser(validatedToken.membership_id);

AsyncStorage.setItem("current_user_id", validatedToken.membership_id)
AsyncStorage.setItem(Store.current_user_ID, validatedToken.membership_id)
.then(() => console.log("saved new user ID"))
.catch((e) => {
console.error("Failed to save user ID", e);
});

const fullToken = await getAccessToken(validatedToken);
console.log("save", `${this.currentUserID}_refresh_token`);
AsyncStorage.setItem(`${this.currentUserID}_refresh_token`, JSON.stringify(fullToken))
.then(() => console.log("saved token"))
console.log("save", `${this.currentUserID}${Store._refresh_token}`);
AsyncStorage.setItem(`${this.currentUserID}${Store._refresh_token}`, JSON.stringify(fullToken))
.then(() => this.setAuthToken(fullToken))
.catch((e) => {
console.error("Failed to save token", e);
});
Expand All @@ -187,13 +177,21 @@ class AuthService {

// This does not delete everything. Logging out should still leave user data behind for when they log back in.
// The 'logout' might simply be the app not being used for so long it needs re-authentication.
async logoutCurrentUser() {
static async logoutCurrentUser() {
console.log("logoutCurrentUser", AuthService.instance.currentUserID);
try {
await AsyncStorage.removeItem("current_user");
await AsyncStorage.removeItem(Store.current_user_ID);
await AsyncStorage.removeItem(`${AuthService.instance.currentUserID}${Store._refresh_token}`);
AuthService.instance.setAuthToken(null);
AuthService.instance.setCurrentUser("");
} catch (e) {
throw new Error("Error removing current user from storage");
}
}

cleanup() {
this.unsubscribe();
}
}

export default AuthService;
13 changes: 13 additions & 0 deletions native_gg/src/authentication/Types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as v from "valibot";

export const refreshTokenSchema = v.object({
access_token: v.string(),
expires_in: v.number(),
membership_id: v.string(),
refresh_expires_in: v.number(),
refresh_token: v.string(),
time_stamp: v.optional(v.string([v.isoTimestamp()])),
token_type: v.string(),
});

export type RefreshToken = v.Output<typeof refreshTokenSchema>;
41 changes: 29 additions & 12 deletions native_gg/src/authentication/Utilities.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import * as base64 from "base-64";
import * as v from "valibot";
import { apiKey, clientID, clientSecret } from "../constants/env.ts";

const refreshTokenSchema = v.object({
access_token: v.string(),
expires_in: v.number(),
membership_id: v.string(),
refresh_expires_in: v.number(),
refresh_token: v.string(),
time_stamp: v.optional(v.string([v.isoTimestamp()])),
token_type: v.string(),
});

type RefreshToken = v.Output<typeof refreshTokenSchema>;
import { RefreshToken, refreshTokenSchema } from "./Types.ts";

export function getRefreshToken(bungieCode: string): Promise<JSON> {
const headers = new Headers();
Expand Down Expand Up @@ -86,3 +75,31 @@ export function getAccessToken(token: RefreshToken): Promise<RefreshToken> {
});
});
}

export function hasAccessExpired(token: RefreshToken): boolean {
// Access lasts 3600 seconds (1 hour)
if (token.time_stamp) {
const lifeTime = token.expires_in;
const timeNow = new Date();
const timeThen = new Date(token.time_stamp);
const secondsLeft = lifeTime - (timeNow.getTime() - timeThen.getTime()) / 1000;
// Count anything less than 5 mins (345 seconds) as expired
return secondsLeft < 345;
}

return true;
}

export function hasRefreshExpired(token: RefreshToken): boolean {
// Refresh lasts 7,776,000 seconds (90 days)
if (token.time_stamp) {
const lifeTime = token.refresh_expires_in;
const timeNow = new Date();
const timeThen = new Date(token.time_stamp);
const secondsLeft = lifeTime - (timeNow.getTime() - timeThen.getTime()) / 1000;
// Count anything less than 5 mins (345 seconds) as expired
return secondsLeft < 345;
}

return true;
}
5 changes: 5 additions & 0 deletions native_gg/src/constants/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// I want to export a name space 'storage' that has several constants such as 'current_ID' and 'auth_token' that are used in the native_gg/src/authentication/AuthService.ts file. I will use the 'export' keyword to export the name space 'storage' and then use the 'export' keyword to export the constants 'current_ID' and 'auth_token' from the name space 'storage'.
export const Store = {
current_user_ID: "current_user_ID",
_refresh_token: "_refresh_token",
};
8 changes: 6 additions & 2 deletions native_gg/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"allowImportingTsExtensions": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noErrorTruncation": true,
}
"noErrorTruncation": true
},
"include": [
"App.tsx",
"src"
]
}

0 comments on commit 7ea3c81

Please sign in to comment.