Jwt rough setup
This commit is contained in:
@@ -1,15 +1,42 @@
|
|||||||
using Seasoned.Backend.Services;
|
using Seasoned.Backend.Services;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Seasoned.Backend.Data;
|
using Seasoned.Backend.Data;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using DotNetEnv;
|
using DotNetEnv;
|
||||||
|
|
||||||
Env.Load("../.env");
|
Env.Load("../.env");
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
var jwtKey = builder.Configuration["Jwt:Key"]
|
||||||
|
?? throw new InvalidOperationException("JWT Key is missing from configuration!");
|
||||||
|
|
||||||
|
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "SeasonedAPI";
|
||||||
|
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "SeasonedFrontend";
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(options => {
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
|
||||||
|
.AddJwtBearer(options => {
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters {
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = jwtIssuer,
|
||||||
|
ValidAudience = jwtAudience,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
||||||
|
|
||||||
@@ -21,22 +48,8 @@ builder.Services.AddIdentityApiEndpoints<IdentityUser>( options => {
|
|||||||
options.Password.RequireLowercase = false;
|
options.Password.RequireLowercase = false;
|
||||||
options.User.RequireUniqueEmail = true;
|
options.User.RequireUniqueEmail = true;
|
||||||
})
|
})
|
||||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
builder.Services.ConfigureApplicationCookie(options =>
|
|
||||||
{
|
|
||||||
options.Cookie.Name = "Seasoned.Session";
|
|
||||||
options.Cookie.HttpOnly = true;
|
|
||||||
options.Cookie.SameSite = SameSiteMode.None;
|
|
||||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
|
||||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
|
|
||||||
options.SlidingExpiration = true;
|
|
||||||
options.Events.OnRedirectToLogin = context =>
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
@@ -93,6 +106,11 @@ using (var scope = app.Services.CreateScope())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||||
|
});
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseCors("SeasonedOriginPolicy");
|
app.UseCors("SeasonedOriginPolicy");
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="dotenv.net" Version="4.0.1" />
|
<PackageReference Include="dotenv.net" Version="4.0.1" />
|
||||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.13" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.13" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
||||||
<PackageReference Include="Mscc.GenerativeAI" Version="2.2.8" />
|
<PackageReference Include="Mscc.GenerativeAI" Version="2.2.8" />
|
||||||
<PackageReference Include="pgvector" Version="0.3.2" />
|
<PackageReference Include="pgvector" Version="0.3.2" />
|
||||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||||
|
|||||||
@@ -50,17 +50,23 @@
|
|||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import '@/assets/css/app-theme.css'
|
import '@/assets/css/app-theme.css'
|
||||||
import SessionTimeout from './components/SessionTimeout.vue'
|
import SessionTimeout from './components/SessionTimeout.vue'
|
||||||
const authCookie = useCookie('.AspNetCore.Identity.Application')
|
|
||||||
const isLoggedIn = useState('isLoggedIn', () => false)
|
const isLoggedIn = useState('isLoggedIn', () => false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (authCookie.value) isLoggedIn.value = true
|
if (import.meta.client) {
|
||||||
|
const token = localStorage.getItem('auth_token')
|
||||||
|
if (token) {
|
||||||
|
isLoggedIn.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
authCookie.value = null
|
|
||||||
isLoggedIn.value = false
|
isLoggedIn.value = false
|
||||||
if (import.meta.client) localStorage.removeItem('token')
|
if (import.meta.client) {
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
navigateTo('/login')
|
navigateTo('/login')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -145,23 +145,27 @@ const handleAuth = async () => {
|
|||||||
authLoading.value = true
|
authLoading.value = true
|
||||||
const endpoint = isLogin.value ? 'api/auth/login' : 'api/auth/register'
|
const endpoint = isLogin.value ? 'api/auth/login' : 'api/auth/register'
|
||||||
|
|
||||||
const url = isLogin.value
|
const url = `${config.public.apiBase}${endpoint}`
|
||||||
? `${config.public.apiBase}${endpoint}?useCookies=true&useSessionCookies=false`
|
|
||||||
: `${config.public.apiBase}${endpoint}`
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await $fetch(url, {
|
const response = await $fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value
|
password: password.value,
|
||||||
},
|
useCookies: false,
|
||||||
credentials: 'include'
|
useSessionCookies: false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isLogin.value) {
|
if (isLogin.value) {
|
||||||
isLoggedIn.value = true
|
if (response.accessToken) {
|
||||||
navigateTo('/')
|
localStorage.setItem('auth_token', response.accessToken)
|
||||||
|
isLoggedIn.value = true
|
||||||
|
navigateTo('/')
|
||||||
|
} else {
|
||||||
|
throw new Error('Token not received')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
isLogin.value = true
|
isLogin.value = true
|
||||||
authLoading.value = false
|
authLoading.value = false
|
||||||
|
|||||||
@@ -3,18 +3,33 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
const isLoggedIn = useState('isLoggedIn');
|
const isLoggedIn = useState('isLoggedIn');
|
||||||
|
|
||||||
nuxtApp.hook('app:created', () => {
|
nuxtApp.hook('app:created', () => {
|
||||||
|
|
||||||
const originalFetch = globalThis.$fetch;
|
const originalFetch = globalThis.$fetch;
|
||||||
|
|
||||||
globalThis.$fetch = originalFetch.create({
|
globalThis.$fetch = originalFetch.create({
|
||||||
|
onRequest({ options }) {
|
||||||
|
if (import.meta.client) {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
if (token) {
|
||||||
|
const headers = new Headers(options.headers);
|
||||||
|
headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
options.headers = headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onResponseError({ response }) {
|
onResponseError({ response }) {
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
console.warn("Session Interceptor: Caught 401 Unauthorized.");
|
console.warn("Session Interceptor: Caught 401 Unauthorized.");
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
if (route.path !== '/login') {
|
if (route.path !== '/login') {
|
||||||
isLoggedIn.value = false;
|
isLoggedIn.value = false;
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
}
|
||||||
|
|
||||||
showTimeout.value = true;
|
showTimeout.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ vi.stubGlobal('$fetch', mockFetch)
|
|||||||
|
|
||||||
const mockNavigate = vi.fn()
|
const mockNavigate = vi.fn()
|
||||||
vi.stubGlobal('navigateTo', mockNavigate)
|
vi.stubGlobal('navigateTo', mockNavigate)
|
||||||
|
vi.stubGlobal('localStorage', { setItem: vi.fn(), getItem: vi.fn(), removeItem: vi.fn() })
|
||||||
|
|
||||||
// Mock Nuxt's useState
|
|
||||||
vi.stubGlobal('useState', () => ({ value: false }))
|
vi.stubGlobal('useState', () => ({ value: false }))
|
||||||
|
|
||||||
describe('LoginPage.vue', () => {
|
describe('LoginPage.vue', () => {
|
||||||
@@ -37,15 +37,12 @@ describe('LoginPage.vue', () => {
|
|||||||
it('switches between Login and Register modes', async () => {
|
it('switches between Login and Register modes', async () => {
|
||||||
const wrapper = mount(LoginPage, mountOptions)
|
const wrapper = mount(LoginPage, mountOptions)
|
||||||
|
|
||||||
// Default is Login
|
|
||||||
expect(wrapper.find('h1').text()).toBe('Sign In')
|
expect(wrapper.find('h1').text()).toBe('Sign In')
|
||||||
expect(wrapper.find('input[label="Confirm Password"]').exists()).toBe(false)
|
expect(wrapper.find('input[label="Confirm Password"]').exists()).toBe(false)
|
||||||
|
|
||||||
// Click toggle
|
|
||||||
await wrapper.find('.auth-toggle-btn').trigger('click')
|
await wrapper.find('.auth-toggle-btn').trigger('click')
|
||||||
|
|
||||||
expect(wrapper.find('h1').text()).toBe('Join Us')
|
expect(wrapper.find('h1').text()).toBe('Join Us')
|
||||||
// V-expand-transition might need a tick or we check the v-if logic
|
|
||||||
expect(wrapper.vm.isLogin).toBe(false)
|
expect(wrapper.vm.isLogin).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ describe('LoginPage.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('calls login API and redirects on success', async () => {
|
it('calls login API and redirects on success', async () => {
|
||||||
mockFetch.mockResolvedValueOnce({ token: 'fake-token' })
|
mockFetch.mockResolvedValueOnce({ accessToken: 'fake-token' })
|
||||||
const wrapper = mount(LoginPage, mountOptions)
|
const wrapper = mount(LoginPage, mountOptions)
|
||||||
const vm = wrapper.vm as any
|
const vm = wrapper.vm as any
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||||
- NUXT_PUBLIC_API_BASE=${NUXT_PUBLIC_API_BASE}
|
- NUXT_PUBLIC_API_BASE=${NUXT_PUBLIC_API_BASE}
|
||||||
|
- Jwt__Key=${JWT__KEY}
|
||||||
|
- Jwt__Issuer=${JWT__ISSUER}
|
||||||
|
- Jwt__Audience=${JWT__AUDIENCE}
|
||||||
- ConnectionStrings__DefaultConnection=${ConnectionStrings__DefaultConnection}
|
- ConnectionStrings__DefaultConnection=${ConnectionStrings__DefaultConnection}
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
- ASPNETCORE_HTTP_PORTS=8080
|
- ASPNETCORE_HTTP_PORTS=8080
|
||||||
|
|||||||
Reference in New Issue
Block a user