Skip to content

Commit

Permalink
updated sn-auth-react with api calls and refactored token refreshing
Browse files Browse the repository at this point in the history
  • Loading branch information
hashtagnulla committed Sep 23, 2024
1 parent 17dd6c8 commit 8a2b4e8
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 53 deletions.
7 changes: 2 additions & 5 deletions apps/sensenet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,5 @@
"semaphore-async-await": "^1.5.1",
"uuid": "9.0.0"
},
"typings": "./dist/index.d.ts",
"resolutions": {
"@types/react": "^18.2.7"
}
}
"typings": "./dist/index.d.ts"
}
2 changes: 1 addition & 1 deletion apps/sensenet/src/context/auth-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function ISAuthProvider({ children }: PropsWithChildren<{}>) {
}

export function SNAuthProvider({ children }: PropsWithChildren<{}>) {
const { user, login, logout } = useSnAuth()
const { user, externalLogin: login, logout } = useSnAuth()

return (
<AuthContext.Provider
Expand Down
8 changes: 4 additions & 4 deletions apps/sensenet/src/context/sn-auth-repository-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getAuthConfig } from '../services/auth-config'

const LoginPage = lazy(() => import(/* webpackChunkName: "login" */ '../components/login/login-page'))

export const authConfigKey = 'sn-oidc-config'
export const authConfigKey = 'sn-auth-config'

export function SnAuthRepositoryProvider({ children }: { children: React.ReactNode }) {
const [isLoginInProgress, setIsLoginInProgress] = useState(false)
Expand Down Expand Up @@ -120,7 +120,7 @@ const RepoProvider = ({
clearAuthState: Function
authServerUrl?: string
}) => {
const { user, login, logout, accessToken, isLoading } = useSnAuth()
const { user, externalLogin, logout, accessToken, isLoading } = useSnAuth()
const logger = useLogger('repo-provider')
const [repo, setRepo] = useState<Repository>()

Expand Down Expand Up @@ -167,7 +167,7 @@ const RepoProvider = ({
const configString = window.localStorage.getItem(authConfigKey)
if (!user && !isLoading && !accessToken && configString) {
try {
await login()
await externalLogin()
} catch (error) {
const config = JSON.parse(configString)
logger.error({ data: error, message: `Couldn't connect to ${config.authority}` })
Expand All @@ -176,7 +176,7 @@ const RepoProvider = ({
}
}
})()
}, [clearAuthState, logger, login, logout, user, isLoading, accessToken])
}, [clearAuthState, logger, externalLogin, logout, user, isLoading, accessToken])

if (!user || !repo) {
return null
Expand Down
13 changes: 11 additions & 2 deletions packages/sn-auth-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@
"build:cjs": "cross-env BABEL_ENV=cjs babel src --extensions '.ts,.tsx' --out-dir 'dist/cjs' --source-maps",
"build:types": "tsc -p tsconfig.json"
},
"peerDependencies": {
"@material-ui/core": "^4.0.0",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"dependencies": {
"@babel/runtime": "^7.18.9",
"@material-ui/core": "^4.12.4",
"tslib": "^2.4.0"
"tslib": "^2.4.0",
"react": "^16.13.0",
"react-dom": "^16.13.0"
},
"devDependencies": {
"@babel/cli": "^7.14.9",
Expand All @@ -43,6 +50,8 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@types/node": "^16.4.10",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"jest": "^29.7.0",
Expand All @@ -52,4 +61,4 @@
"ts-jest": "^27.0.5",
"typescript": "~4.7.4"
}
}
}
178 changes: 153 additions & 25 deletions packages/sn-auth-react/src/components/authentication-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { createContext, ReactNode, useState, useEffect } from 'react'
import { User } from '../models/user'
import { AuthRoutes } from './auth-routes'
import { SnAuthConfiguration } from '../models/sn-auth-configuration'
import { convertAuthTokenApiCall, getUserDetailsApiCall, logoutApiCall, refreshTokenApiCall } from '../server-actions'
import { changePasswordApiCall, convertAuthTokenApiCall, forgotPasswordApiCall, getUserDetailsApiCall, loginApiCall, logoutApiCall, multiFactorApiCall, passwordRecoveryApiCall, refreshTokenApiCall, validateTokenApiCall } from '../server-actions'
import {
getAccessToken,
getRefreshToken,
Expand All @@ -14,13 +14,22 @@ import {
setRefreshToken as setRefreshTokenStorage,
setUserDetails as setUserDetailsStorage,
} from '../storageHelpers'
import { LoginRequest } from '../models/login-request'
import { LoginResponse } from '../models/login-response'
import { MultiFactorLoginRequest } from '../models/multi-factor-login-request'

export interface AuthenticationContextState {
isLoading: boolean
user: User | null
login: () => void
login: (loginRequest: LoginRequest) => Promise<LoginResponse>
externalLogin: () => void
multiFactorLogin: (multiFactorRequest: MultiFactorLoginRequest) => void
forgotPassword: (email: string) => Promise<void>,
passwordRecovery: (token: string, password: string) => Promise<void>,
changePassword: (password: string) => Promise<void>
logout: () => void
accessToken: string | null
error: string | null
}

export const AuthenticationContext = createContext<AuthenticationContextState | undefined>(undefined)
Expand Down Expand Up @@ -70,6 +79,8 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {
const [user, setUser] = useState<User | null>(getUserDetails())
const [path, setPath] = useState<string>(window.location.pathname)
const [isLoading, setIsLoading] = useState<boolean>(true)
const [isRefreshingToken, setIsRefreshingToken] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)

const setNewPath = () => setPath(window.location.pathname)

Expand All @@ -86,27 +97,69 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {
}
}, [path])


useEffect(() => {
const checkTokenExpiry = async () => {
setIsLoading(true)
if (isTokenAboutToExpire(accessToken)) {
await refreshAccessToken()
}
const validateAndRefreshToken = async () => {
setIsLoading(true);
try {
if (accessToken) {
let accessTokenLocal = accessToken
const isValid = await validateTokenApiCall(props.authServerUrl, accessTokenLocal);

if (!isValid) {
const response = await refreshAccessToken();
if (!response?.accessTokenResponse)
throw new Error()

const intervalId = setInterval(async () => {
if (isTokenAboutToExpire(accessToken)) {
await refreshAccessToken()
accessTokenLocal = response.accessTokenResponse
}

const userDetails = await getUserDetailsApiCall(props.authServerUrl, accessTokenLocal);
setUser(userDetails);
}
}, 5000)

setIsLoading(false)
return () => clearInterval(intervalId)
setIsLoading(false);
} catch (err) {
setError('Failed to validate or refresh token');
setIsLoading(false);
}
};

validateAndRefreshToken();
}, []);

useEffect(() => {
const intervalId = setInterval(async () => {
const accToken = getAccessToken()
if (accToken && isTokenAboutToExpire(accToken) && !isRefreshingToken) {
setIsRefreshingToken(true)
}
}, TOKEN_EXPIRY_THRESHOLD);

return () => clearInterval(intervalId);
}, [isRefreshingToken])

useEffect(() => {
const refreshToken = async () => {
try {
const response = await refreshAccessToken();
if (!response?.accessTokenResponse)
throw new Error()

const userDetails = await getUserDetailsApiCall(props.authServerUrl, response.accessTokenResponse);
setUser(userDetails);
} catch (err) {
setError('Failed to refresh access token');
logoutLocal();
}
setIsRefreshingToken(false)
}

checkTokenExpiry()
}, [accessToken, refreshToken])
if (isRefreshingToken)
refreshToken()
}, [isRefreshingToken, props.authServerUrl])

const login = () => {
const externalLogin = () => {
window.location.replace(
`${props.authServerUrl}/Login?RedirectUrl=${window.location.origin}&CallbackUri=${props.snAuthConfiguration.callbackUri}`,
)
Expand All @@ -119,20 +172,18 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {

try {
if (authToken) {
const { accessToken: accessTokenResponse, refreshToken: refreshTokenRepsonse } = await convertAuthTokenApiCall(
const { accessToken: accessTokenResponse, refreshToken: refreshTokenResponse } = await convertAuthTokenApiCall(
props.authServerUrl,
authToken,
)
setAccessToken(accessTokenResponse)
setRefreshToken(refreshTokenRepsonse)
setAccessTokenStorage(accessTokenResponse)
setRefreshTokenStorage(refreshTokenRepsonse)
setAccessAndRefreshToken(accessTokenResponse, refreshTokenResponse)

const user = await getUserDetailsApiCall(props.authServerUrl, accessTokenResponse)
setUser(user)
setUserDetailsStorage(user)
}
} catch (e) {
setError(e);
console.error(e)
} finally {
setIsLoading(false)
Expand All @@ -147,13 +198,13 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {
props.authServerUrl,
refreshToken,
)
setAccessToken(accessTokenResponse)
setRefreshToken(refreshTokenResponse)
setAccessAndRefreshToken(accessTokenResponse, refreshTokenResponse)

setAccessTokenStorage(accessTokenResponse)
setRefreshTokenStorage(refreshTokenResponse)
return { accessTokenResponse, refreshTokenResponse }
} catch (e) {
console.error(e)
setError("Failed to refresh token")
logoutLocal()
} finally {
setIsLoading(false)
}
Expand All @@ -174,6 +225,69 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {
else logoutLocal()
}

const login = async (loginRequest: LoginRequest): Promise<LoginResponse> => {
try {
const response = await loginApiCall(props.authServerUrl, loginRequest)

if (!response.multiFactorRequired && response.accessToken && response.refreshToken) {
setAccessAndRefreshToken(response.accessToken, response.refreshToken)

const user = await getUserDetailsApiCall(props.authServerUrl, response.accessToken)
setUser(user)
setUserDetailsStorage(user)

return response
}
else {
throw new Error()
}
}
catch (e) {
console.log("Error during login.")

removeAccessToken()
removeRefreshToken()

throw e;
}
}

const multiFactorLogin = async (multiFactorRequest: MultiFactorLoginRequest): Promise<LoginResponse> => {
try {
const response = await multiFactorApiCall(props.authServerUrl, multiFactorRequest)

if (response.accessToken && response.refreshToken) {
setAccessAndRefreshToken(response.accessToken, response.refreshToken)

return response;
}
else {
throw new Error();
}
}
catch (e) {
console.log("Error during multi-factor validation.")

removeAccessToken()
removeRefreshToken()

throw e;
}
}

const forgotPassword = async (email: string) => {
await forgotPasswordApiCall(props.authServerUrl, { email })
}

const passwordRecovery = async (token: string, password: string) => {
await passwordRecoveryApiCall(props.authServerUrl, { token, password })
}

const changePassword = async (password: string) => {
if (accessToken)
changePasswordApiCall(props.authServerUrl, accessToken, { password })
}

const logoutLocal = () => {
setAccessToken(null)
setRefreshToken(null)
Expand All @@ -186,14 +300,28 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {
window.location.replace('/')
}

const setAccessAndRefreshToken = (accessToken: string, refreshToken: string) => {
setAccessToken(accessToken)
setRefreshToken(refreshToken)

setAccessTokenStorage(accessToken)
setRefreshTokenStorage(refreshToken)
}

return (
<AuthenticationContext.Provider
value={{
accessToken,
user,
login,
externalLogin,
logout,
forgotPassword,
passwordRecovery,
changePassword,
multiFactorLogin,
isLoading,
error
}}>
<AuthRoutes callbackUri={props.snAuthConfiguration.callbackUri} currentPath={path}>
{props.children}
Expand Down
3 changes: 3 additions & 0 deletions packages/sn-auth-react/src/models/change-password-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ChangePasswordRequest {
password: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ForgottenPasswordRequest {
email: string;
passwordRecoveryUrl?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface MultiFactorLoginRequest {
multiFactorAuthToken: string;
multiFactorCode: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface PasswordRecoveryRequest {
password: string;
token: string;
}
Loading

0 comments on commit 8a2b4e8

Please sign in to comment.