-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add custom authn/authz (TT-1547) (#52)
Før deployment av denne må auth API deployes (se relatert Jira-ticket).
- Loading branch information
1 parent
2c81847
commit f4dec32
Showing
23 changed files
with
488 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
NEXT_PUBLIC_BASE_PATH=/hugin | ||
NEXT_PUBLIC_KEYCLOAK_BASE_URL=https://your-keycloak-url.org | ||
NEXT_PUBLIC_KEYCLOAK_REALM=your-realm | ||
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=your-client-id | ||
|
||
CATALOGUE_API_PATH=http://localhost:8087/bikube | ||
DATABASE_URL='' | ||
AUTH_API_PATH=http://localhost:8088/auth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
'use client'; | ||
|
||
import {createContext, useCallback, useContext, useEffect, useState} from 'react'; | ||
import {useRouter} from 'next/navigation'; | ||
import keycloakConfig from '@/lib/keycloak'; | ||
import {User} from '@/models/UserToken'; | ||
import {refresh, signIn, signOut} from '@/services/auth.data'; | ||
|
||
interface IAuthContext { | ||
authenticated: boolean; | ||
user?: User; | ||
logout?: () => void; | ||
} | ||
|
||
const AuthContext = createContext<IAuthContext>({ | ||
authenticated: false, | ||
logout: () => {} | ||
}); | ||
|
||
export const AuthProvider = ({children}: { children: React.ReactNode }) => { | ||
const router = useRouter(); | ||
|
||
const [authenticated, setAuthenticated] = useState<boolean>(false); | ||
const [user, setUser] = useState<User>(); | ||
const [intervalId, setIntervalId] = useState<number>(); | ||
|
||
const handleNotAuthenticated = useCallback(() => { | ||
setAuthenticated(false); | ||
setUser(undefined); | ||
if (intervalId) { | ||
clearInterval(intervalId); | ||
} | ||
const currentUrl = window.location.href; | ||
window.location.assign(`${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/auth` + | ||
`?client_id=${keycloakConfig.clientId}&redirect_uri=${currentUrl}&response_type=code&scope=openid`); | ||
}, [intervalId]); | ||
|
||
useEffect(() => { | ||
const codeInParams = new URLSearchParams(window.location.search).get('code'); | ||
if (codeInParams) { | ||
const redirectUrl = new URLSearchParams({redirectUrl: trimRedirectUrl(window.location.href)}).toString(); | ||
void signIn(codeInParams, redirectUrl).then((token: User) => { | ||
handleIsAuthenticated(token); | ||
router.push('/'); | ||
}).catch((e: Error) => { | ||
console.error('Failed to sign in: ', e.message); | ||
handleNotAuthenticated(); | ||
}); | ||
} else if (user) { | ||
if (user.expires && new Date(user.expires) > new Date()) { | ||
handleIsAuthenticated(user); | ||
} | ||
} else { | ||
handleNotAuthenticated(); | ||
const currentUrl = window.location.href; | ||
window.location.assign(`${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/auth` + | ||
`?client_id=${keycloakConfig.clientId}&redirect_uri=${currentUrl}&response_type=code&scope=openid`); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
const handleIsAuthenticated = (newUser: User) => { | ||
if (newUser) { | ||
setUser(newUser); | ||
setAuthenticated(true); | ||
} | ||
}; | ||
|
||
const refreshToken = useCallback(async () => { | ||
return refresh(); | ||
}, []); | ||
|
||
const setIntervalToRefreshAccessToken = useCallback(async () => { | ||
if (user?.expires && !intervalId) { | ||
const expiryTime = new Date(user?.expires).getTime() - Date.now(); | ||
if (expiryTime < 1000 * 60 * 4.75) { | ||
await refreshToken(); | ||
} | ||
setIntervalId(window.setInterval(() => { | ||
void refreshToken().then((newUser: User) => { | ||
handleIsAuthenticated(newUser); | ||
}) | ||
.catch((e: Error) => { | ||
console.error('Failed to refresh token: ', e.message); | ||
handleNotAuthenticated(); | ||
}); | ||
}, (1000 * 60 * 4.75))); // Refresh every 4.75 minutes (fifteen seconds before expiry) | ||
} | ||
}, [handleNotAuthenticated, intervalId, refreshToken, user?.expires]); | ||
|
||
useEffect(() => { | ||
void setIntervalToRefreshAccessToken(); | ||
}, [setIntervalToRefreshAccessToken]); | ||
|
||
const trimRedirectUrl= (returnUrl: string): string => { | ||
returnUrl = returnUrl.split('?')[0]; | ||
if (returnUrl.at(-1) === '/') { | ||
returnUrl = returnUrl.slice(0, -1); | ||
} | ||
return returnUrl; | ||
}; | ||
|
||
const logout = async () => { | ||
await signOut() | ||
.then(() => { | ||
handleNotAuthenticated(); | ||
}); | ||
}; | ||
|
||
return ( | ||
<AuthContext.Provider | ||
value={{ | ||
authenticated, | ||
user, | ||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||
logout | ||
}} | ||
> | ||
{children} | ||
</AuthContext.Provider> | ||
); | ||
}; | ||
|
||
export const useAuth = () => useContext<IAuthContext>(AuthContext); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import {NextResponse} from 'next/server'; | ||
import {User, UserToken} from '@/models/UserToken'; | ||
import {getRefreshToken, setUserCookie} from '@/utils/cookieUtils'; | ||
|
||
// POST api/auth/refresh | ||
export async function POST(): Promise<NextResponse> { | ||
const refreshToken = getRefreshToken(); | ||
if (!refreshToken) { | ||
return NextResponse.json({error: 'No user token found'}, {status: 401}); | ||
} | ||
|
||
const data = await fetch(`${process.env.AUTH_API_PATH}/refresh`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: refreshToken | ||
}); | ||
|
||
const newToken = await data.json() as UserToken; | ||
|
||
if (!newToken || !newToken.name || !newToken.expires) { | ||
return NextResponse.json({error: 'Failed to refresh token'}, {status: 500}); | ||
} | ||
|
||
setUserCookie(newToken); | ||
|
||
const user: User = {name: newToken.name, expires: newToken.expires}; | ||
return NextResponse.json(user, {status: 200}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import {NextRequest, NextResponse} from 'next/server'; | ||
import {User, UserToken} from '@/models/UserToken'; | ||
import {ProblemDetail} from '@/models/ProblemDetail'; | ||
import {setUserCookie} from '@/utils/cookieUtils'; | ||
|
||
interface LoginRequest { | ||
code: string; | ||
redirectUrl: string; | ||
} | ||
|
||
// POST api/auth/signin | ||
export async function POST(req: NextRequest): Promise<NextResponse> { | ||
const {code, redirectUrl} = await req.json() as LoginRequest; | ||
const data = await fetch(`${process.env.AUTH_API_PATH}/login?${redirectUrl}`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: code | ||
}) | ||
.then(async response => { | ||
if (!response.ok) { | ||
const problemDetail = await response.json() as ProblemDetail; | ||
return NextResponse.json({error: problemDetail.detail}, {status: problemDetail.status}); | ||
} | ||
return response; | ||
}); | ||
|
||
if (data instanceof NextResponse) { | ||
return data; | ||
} | ||
|
||
const userToken = await data.json() as UserToken; | ||
|
||
if (!userToken || !userToken.name || !userToken.expires) { | ||
return NextResponse.json({error: 'Failed to authenticate'}, {status: 500}); | ||
} | ||
|
||
setUserCookie(userToken); | ||
|
||
const user: User = {name: userToken.name, expires: userToken.expires}; | ||
return NextResponse.json(user, {status: 200}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import {NextResponse} from 'next/server'; | ||
import {deleteUserToken, getRefreshToken} from '@/utils/cookieUtils'; | ||
|
||
// POST api/auth/signout | ||
export async function POST(): Promise<NextResponse> { | ||
const refreshToken = getRefreshToken(); | ||
if (!refreshToken) { | ||
return NextResponse.json({error: 'No user token found'}, {status: 401}); | ||
} | ||
|
||
return await fetch(`${process.env.AUTH_API_PATH}/logout`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: refreshToken | ||
}).then(res => { | ||
if (!res.ok) { | ||
return NextResponse.json({error: 'Failed to logout'}, {status: res.status}); | ||
} | ||
deleteUserToken(); | ||
return NextResponse.json({message: 'Logged out successfully'}, {status: 200}); | ||
}).catch((error: Error) => { | ||
return NextResponse.json({error: `Failed to logout: ${error.message}`}, {status: 500}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,14 @@ | ||
'use client'; | ||
|
||
import {NextUIProvider} from '@nextui-org/react'; | ||
import {AuthProvider} from '@/app/AuthProvider'; | ||
|
||
export function Providers({children}: { children: React.ReactNode }) { | ||
return ( | ||
<NextUIProvider locale='nb-NO'> | ||
{children} | ||
</NextUIProvider> | ||
<AuthProvider> | ||
<NextUIProvider locale='nb-NO'> | ||
{children} | ||
</NextUIProvider> | ||
</AuthProvider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import {useAuth} from '@/app/AuthProvider'; | ||
import {Button} from '@nextui-org/react'; | ||
import {FaSignOutAlt} from 'react-icons/fa'; | ||
|
||
const LogoutButton = () => { | ||
const { logout } = useAuth(); | ||
|
||
return ( | ||
<Button | ||
className="edit-button-style" | ||
endContent={<FaSignOutAlt size={25} />} | ||
onClick={logout} | ||
> | ||
Logg ut | ||
</Button> | ||
); | ||
}; | ||
|
||
export default LogoutButton; |
Oops, something went wrong.