-
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.
- Loading branch information
Showing
10 changed files
with
291 additions
and
38 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
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,36 @@ | ||
using GeoCop.Api.Authorization; | ||
using GeoCop.Api.Models; | ||
using Microsoft.AspNetCore.Authorization; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace GeoCop.Api.Controllers; | ||
|
||
/// <summary> | ||
/// Controller for user information. | ||
/// </summary> | ||
[Authorize(Policy = GeocopPolicies.User)] | ||
[ApiController] | ||
[Route("api/v{version:apiVersion}/[controller]")] | ||
public class UserController : ControllerBase | ||
{ | ||
private readonly Context context; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="UserController"/> class. | ||
/// </summary> | ||
/// <param name="context">The database context.</param> | ||
public UserController(Context context) | ||
{ | ||
this.context = context; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the current user information. | ||
/// </summary> | ||
/// <returns>The <see cref="User"/> that is currently logged in.</returns> | ||
[HttpGet] | ||
public async Task<User?> GetAsync() | ||
{ | ||
return await context.GetUserByPrincipalAsync(User); | ||
} | ||
} |
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,107 @@ | ||
import { PublicClientApplication } from "@azure/msal-browser"; | ||
import { MsalProvider } from "@azure/msal-react"; | ||
import { createContext, useCallback, useState, useMemo, useEffect, useRef } from "react"; | ||
|
||
const authDefault = { | ||
user: undefined, | ||
login: () => {}, | ||
logout: () => {}, | ||
}; | ||
|
||
export const AuthContext = createContext(authDefault); | ||
|
||
export const AuthProvider = ({ children, authScopes, oauth }) => { | ||
const msalInstance = useMemo(() => { | ||
return new PublicClientApplication(oauth ?? {}); | ||
}, [oauth]); | ||
|
||
const [user, setUser] = useState(); | ||
const loginSilentIntervalRef = useRef(); | ||
|
||
const fetchUserInfo = useCallback(async () => { | ||
const userResult = await fetch("/api/v1/user"); | ||
if (!userResult.ok) throw new Error(userResult.statusText); | ||
|
||
const userJson = await userResult.json(); | ||
setUser({ name: userJson.fullName, isAdmin: userJson.isAdmin }); | ||
}, [setUser]); | ||
|
||
const loginCompleted = useCallback( | ||
async (idToken) => { | ||
document.cookie = `geocop.auth=${idToken};Path=/;Secure`; | ||
await fetchUserInfo(); | ||
}, | ||
[fetchUserInfo], | ||
); | ||
|
||
const logoutCompleted = useCallback(() => { | ||
msalInstance.setActiveAccount(null); | ||
clearInterval(loginSilentIntervalRef.current); | ||
document.cookie = "geocop.auth=;expires=Thu, 01 Jan 1970 00:00:00 GMT;Path=/;Secure"; | ||
setUser(undefined); | ||
}, [setUser, msalInstance]); | ||
|
||
const loginSilent = useCallback(async () => { | ||
try { | ||
await msalInstance.initialize(); | ||
const result = await msalInstance.acquireTokenSilent({ | ||
scopes: authScopes, | ||
}); | ||
loginCompleted(result.idToken); | ||
} catch (error) { | ||
console.warn("Failed to refresh authentication.", error); | ||
logoutCompleted(); | ||
} | ||
}, [msalInstance, authScopes, loginCompleted, logoutCompleted]); | ||
|
||
const setRefreshTokenInterval = useCallback(() => { | ||
clearInterval(loginSilentIntervalRef.current); | ||
loginSilentIntervalRef.current = setInterval(loginSilent, 1000 * 60 * 5); | ||
}, [loginSilent]); | ||
|
||
// Fetch user info after reload | ||
const activeAccount = msalInstance.getActiveAccount(); | ||
const hasActiveAccount = activeAccount !== null; | ||
useEffect(() => { | ||
if (hasActiveAccount && !user) { | ||
loginSilent(); | ||
setRefreshTokenInterval(); | ||
} | ||
}, [hasActiveAccount, user, loginSilent, setRefreshTokenInterval]); | ||
|
||
async function login() { | ||
try { | ||
const result = await msalInstance.loginPopup({ | ||
scopes: authScopes, | ||
}); | ||
msalInstance.setActiveAccount(result.account); | ||
loginCompleted(result.idToken); | ||
setRefreshTokenInterval(); | ||
} catch (error) { | ||
console.warn(error); | ||
} | ||
} | ||
|
||
async function logout() { | ||
try { | ||
await msalInstance.logoutPopup(); | ||
logoutCompleted(); | ||
} catch (error) { | ||
console.warn(error); | ||
} | ||
} | ||
|
||
return ( | ||
<MsalProvider instance={msalInstance}> | ||
<AuthContext.Provider | ||
value={{ | ||
user, | ||
login, | ||
logout, | ||
}} | ||
> | ||
{children} | ||
</AuthContext.Provider> | ||
</MsalProvider> | ||
); | ||
}; |
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,4 @@ | ||
import { useContext } from "react"; | ||
import { AuthContext } from "./AuthContext"; | ||
|
||
export const useAuth = () => useContext(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,25 @@ | ||
using GeoCop.Api.Models; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Moq; | ||
using System.Security.Claims; | ||
|
||
namespace GeoCop.Api.Test; | ||
|
||
internal static class ControllerExtensions | ||
{ | ||
public static Mock<HttpContext> SetupTestUser(this ControllerBase controller, User user) | ||
{ | ||
var httpContextMock = new Mock<HttpContext>(); | ||
controller.ControllerContext.HttpContext = httpContextMock.Object; | ||
|
||
var principal = new ClaimsPrincipal(new ClaimsIdentity(new[] | ||
{ | ||
new Claim(ContextExtensions.UserIdClaim, user.AuthIdentifier), | ||
new Claim(ContextExtensions.NameClaim, user.FullName), | ||
new Claim(ContextExtensions.EmailClaim, user.Email), | ||
})); | ||
httpContextMock.SetupGet(c => c.User).Returns(principal); | ||
return httpContextMock; | ||
} | ||
} |
Oops, something went wrong.