mirror of
https://github.com/mvvasilev/personal-finances.git
synced 2025-04-19 14:19:52 +03:00
Add SSL configs, configure session cookies ( mostly defaults ), fix login/logout on frontend
This commit is contained in:
parent
6a5ea58e08
commit
da3f2f6169
9 changed files with 199 additions and 91 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -43,3 +43,4 @@ bin/
|
||||||
|
|
||||||
### .env files ###
|
### .env files ###
|
||||||
.env
|
.env
|
||||||
|
*.p12
|
|
@ -1,9 +1,17 @@
|
||||||
AUTHENTIK_CLIENT_ID= authentik oauth2 client id
|
AUTHENTIK_CLIENT_ID= authentik oauth2 client id
|
||||||
AUTHENTIK_CLIENT_SECRET= authentik oauth2 client secret
|
AUTHENTIK_CLIENT_SECRET= authentik oauth2 client secret
|
||||||
|
AUTHENTIK_ISSUER_URL= authentik issuer url ( dev: https://auth.mvvasilev.dev/application/o/personal-finances/ )
|
||||||
|
AUTHENTIK_BACK_CHANNEL_LOGOUT_URL= authentik back channel logout url ( dev: https://auth.mvvasilev.dev/application/o/personal-finances/end-session/ )
|
||||||
|
|
||||||
GATEWAY_URI= http://localhost:8080
|
GATEWAY_URI= http://localhost:8080
|
||||||
API_URI= http://localhost:8081
|
API_URI= http://localhost:8081
|
||||||
FRONTEND_URI= http://localhost:5173
|
FRONTEND_URI= http://localhost:5173
|
||||||
|
|
||||||
|
SSL_ENABLED= true if generated an ssl cert ( keytool -genkeypair -alias local -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore local.p12 -validity 3650 )
|
||||||
|
SSL_KEY_STORE_TYPE= type of key store ( PKCS12 )
|
||||||
|
SSL_KEY_STORE= where to find the key store ( classpath:keystore/local.p12 if located in main/resources/keystore/local.p12 )
|
||||||
|
SSL_KEY_STORE_PASSWORD= the password for the key store
|
||||||
|
SSL_KEY_ALIAS= the key store alias
|
||||||
|
|
||||||
REDIS_HOST= the address of the redis host
|
REDIS_HOST= the address of the redis host
|
||||||
REDIS_PORT= the port of redis ( 6379 by default )
|
REDIS_PORT= the port of redis ( 6379 by default )
|
|
@ -8,6 +8,7 @@ import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseCookie;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
@ -19,17 +20,28 @@ import org.springframework.security.oauth2.client.web.server.ServerOAuth2Authori
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
||||||
|
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||||
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
|
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
|
||||||
import org.springframework.web.server.WebSession;
|
import org.springframework.web.server.WebSession;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@EnableRedisWebSession
|
@EnableRedisWebSession
|
||||||
public class SecurityConfiguration implements BeanClassLoaderAware {
|
public class SecurityConfiguration implements BeanClassLoaderAware {
|
||||||
|
|
||||||
|
public static final String IS_LOGGED_IN_COOKIE = "isLoggedIn";
|
||||||
|
|
||||||
@Value("${spring.security.oauth2.client.provider.authentik.back-channel-logout-url}")
|
@Value("${spring.security.oauth2.client.provider.authentik.back-channel-logout-url}")
|
||||||
private String backChannelLogoutUrl;
|
private String backChannelLogoutUrl;
|
||||||
|
|
||||||
|
@Value("${server.reactive.session.cookie.max-age}")
|
||||||
|
private Duration springSessionDuration;
|
||||||
|
|
||||||
|
@Value("${server.reactive.session.cookie.path}")
|
||||||
|
private String springSessionPath;
|
||||||
|
|
||||||
private ClassLoader loader;
|
private ClassLoader loader;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -41,6 +53,23 @@ public class SecurityConfiguration implements BeanClassLoaderAware {
|
||||||
c.pathMatchers("/api/**").authenticated();
|
c.pathMatchers("/api/**").authenticated();
|
||||||
})
|
})
|
||||||
.oauth2Login(c -> {
|
.oauth2Login(c -> {
|
||||||
|
c.authenticationSuccessHandler((exchange, auth) -> {
|
||||||
|
ServerHttpResponse response = exchange.getExchange().getResponse();
|
||||||
|
|
||||||
|
response.getCookies().set(
|
||||||
|
IS_LOGGED_IN_COOKIE,
|
||||||
|
ResponseCookie.from(IS_LOGGED_IN_COOKIE)
|
||||||
|
.value("true")
|
||||||
|
.path(springSessionPath)
|
||||||
|
.maxAge(springSessionDuration)
|
||||||
|
.httpOnly(false)
|
||||||
|
.secure(false)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
return new RedirectServerAuthenticationSuccessHandler("/").onAuthenticationSuccess(exchange, auth);
|
||||||
|
});
|
||||||
|
|
||||||
c.authorizationRequestResolver(resolver);
|
c.authorizationRequestResolver(resolver);
|
||||||
})
|
})
|
||||||
.logout(c -> {
|
.logout(c -> {
|
||||||
|
@ -50,6 +79,17 @@ public class SecurityConfiguration implements BeanClassLoaderAware {
|
||||||
response.setStatusCode(HttpStatus.SEE_OTHER);
|
response.setStatusCode(HttpStatus.SEE_OTHER);
|
||||||
response.getHeaders().set("Location", backChannelLogoutUrl);
|
response.getHeaders().set("Location", backChannelLogoutUrl);
|
||||||
response.getCookies().remove("JSESSIONID");
|
response.getCookies().remove("JSESSIONID");
|
||||||
|
response.getCookies().remove("SESSION");
|
||||||
|
response.getCookies().set(
|
||||||
|
IS_LOGGED_IN_COOKIE,
|
||||||
|
ResponseCookie.from(IS_LOGGED_IN_COOKIE)
|
||||||
|
.value("false")
|
||||||
|
.path(springSessionPath)
|
||||||
|
.maxAge(springSessionDuration)
|
||||||
|
.httpOnly(false)
|
||||||
|
.secure(false)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
return ex.getExchange().getSession().flatMap(WebSession::invalidate);
|
return ex.getExchange().getSession().flatMap(WebSession::invalidate);
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ spring:
|
||||||
redirect-uri: "{baseUrl}/login/oauth2/code/authentik"
|
redirect-uri: "{baseUrl}/login/oauth2/code/authentik"
|
||||||
provider:
|
provider:
|
||||||
authentik:
|
authentik:
|
||||||
back-channel-logout-url: ${AUTHENTIK_BACK_CHANNEL_LOGOUT_URL}
|
back-channel-logout-url: ${AUTHENTIK_BACK_CHANNEL_LOGOUT_URL} # spring doesn't support back-channel logouts by default
|
||||||
issuer-uri: ${AUTHENTIK_ISSUER_URL}
|
issuer-uri: ${AUTHENTIK_ISSUER_URL}
|
||||||
cloud:
|
cloud:
|
||||||
gateway:
|
gateway:
|
||||||
|
@ -26,7 +26,7 @@ spring:
|
||||||
routes:
|
routes:
|
||||||
- id: api
|
- id: api
|
||||||
uri: ${API_URI}
|
uri: ${API_URI}
|
||||||
order: 0
|
order: 1
|
||||||
predicates:
|
predicates:
|
||||||
- Path=/api/**
|
- Path=/api/**
|
||||||
filters:
|
filters:
|
||||||
|
@ -36,3 +36,18 @@ spring:
|
||||||
uri: ${FRONTEND_URI}
|
uri: ${FRONTEND_URI}
|
||||||
predicates:
|
predicates:
|
||||||
- Path=/**
|
- Path=/**
|
||||||
|
server:
|
||||||
|
ssl:
|
||||||
|
enabled: true
|
||||||
|
key-store-type: PKCS12
|
||||||
|
key-store: classpath:keystore/local.p12
|
||||||
|
key-store-password: asdf1234
|
||||||
|
key-alias: local
|
||||||
|
reactive:
|
||||||
|
session:
|
||||||
|
cookie:
|
||||||
|
http-only: true
|
||||||
|
secure: true
|
||||||
|
same-site: lax
|
||||||
|
max-age: 30m
|
||||||
|
path: "/"
|
|
@ -1,4 +1,4 @@
|
||||||
JWT_ISSUER_URL= auth server configuration url for fetching JWKs
|
AUTHENTIK_ISSUER_URL= auth server configuration url for fetching JWKs ( dev: https://auth.mvvasilev.dev/application/o/personal-finances/ )
|
||||||
|
|
||||||
DATASOURCE_URL= database jdbc url ( postgres only, example: jdbc:postgresql://localhost:45093/mydatabase )
|
DATASOURCE_URL= database jdbc url ( postgres only, example: jdbc:postgresql://localhost:45093/mydatabase )
|
||||||
DATASOURCE_USER= database user
|
DATASOURCE_USER= database user
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -26,7 +27,7 @@ public class ApiController {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/api/user-info")
|
@PostMapping("/api/user-info")
|
||||||
public ResponseEntity<Authentication> userInfo(JwtAuthenticationToken authenticationToken) {
|
public ResponseEntity<Authentication> userInfo(JwtAuthenticationToken authenticationToken) {
|
||||||
logger.info(authenticationToken.getToken().getClaimAsString(JwtClaimNames.SUB));
|
logger.info(authenticationToken.getToken().getClaimAsString(JwtClaimNames.SUB));
|
||||||
return ResponseEntity.of(Optional.of(SecurityContextHolder.getContext().getAuthentication()));
|
return ResponseEntity.of(Optional.of(SecurityContextHolder.getContext().getAuthentication()));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import CssBaseline from '@mui/material/CssBaseline'
|
import CssBaseline from '@mui/material/CssBaseline'
|
||||||
import { ThemeProvider, createTheme } from '@mui/material';
|
import {ThemeProvider, createTheme} from '@mui/material';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Drawer from '@mui/material/Drawer';
|
import Drawer from '@mui/material/Drawer';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
|
@ -10,92 +10,128 @@ import ListItem from '@mui/material/ListItem';
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import { Home as HomeIcon } from '@mui/icons-material';
|
import {Home as HomeIcon} from '@mui/icons-material';
|
||||||
import { Receipt as TransactionsIcon } from '@mui/icons-material';
|
import {Receipt as TransactionsIcon} from '@mui/icons-material';
|
||||||
import { Logout as LogoutIcon } from '@mui/icons-material';
|
import {Logout as LogoutIcon} from '@mui/icons-material';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import {Login as LoginIcon} from "@mui/icons-material";
|
||||||
|
import {Toaster} from 'react-hot-toast';
|
||||||
import theme from '../components/ThemeRegistry/theme';
|
import theme from '../components/ThemeRegistry/theme';
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
const DRAWER_WIDTH = 240;
|
const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
const NAV_LINKS = [
|
const NAV_LINKS = [
|
||||||
{ text: 'Home', to: '/', icon: HomeIcon },
|
{text: 'Home', to: '/', icon: HomeIcon},
|
||||||
{ text: 'Transactions', to: '/transactions', icon: TransactionsIcon },
|
{text: 'Transactions', to: '/transactions', icon: TransactionsIcon},
|
||||||
];
|
];
|
||||||
|
|
||||||
const BOTTOM_LINKS = [
|
const BOTTOM_LINKS = [
|
||||||
{ text: 'Logout', icon: LogoutIcon, href: "/logout" },
|
// {
|
||||||
|
// text: 'Logout',
|
||||||
|
// icon: LogoutIcon,
|
||||||
|
// //href: "/logout",
|
||||||
|
// onClick: (e) => {
|
||||||
|
// console.log(e);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
function getCookie(key) {
|
||||||
return (
|
var b = document.cookie.match("(^|;)\\s*" + key + "\\s*=\\s*([^;]+)");
|
||||||
<ThemeProvider theme={theme}>
|
return b ? b.pop() : "";
|
||||||
<CssBaseline />
|
}
|
||||||
<Drawer
|
|
||||||
sx={{
|
function isLoggedIn() {
|
||||||
width: DRAWER_WIDTH,
|
return getCookie("isLoggedIn") === "true";
|
||||||
flexShrink: 0,
|
}
|
||||||
'& .MuiDrawer-paper': {
|
|
||||||
width: DRAWER_WIDTH,
|
export default function RootLayout({children}) {
|
||||||
boxSizing: 'border-box',
|
return (
|
||||||
},
|
<ThemeProvider theme={theme}>
|
||||||
}}
|
<CssBaseline/>
|
||||||
variant="permanent"
|
<Drawer
|
||||||
anchor="left"
|
sx={{
|
||||||
>
|
width: DRAWER_WIDTH,
|
||||||
<List>
|
flexShrink: 0,
|
||||||
{NAV_LINKS.map(({ text, to, icon: Icon }) => (
|
'& .MuiDrawer-paper': {
|
||||||
<ListItem key={to} disablePadding>
|
width: DRAWER_WIDTH,
|
||||||
<ListItemButton component={Link} to={to}>
|
boxSizing: 'border-box',
|
||||||
<ListItemIcon>
|
},
|
||||||
<Icon />
|
}}
|
||||||
</ListItemIcon>
|
variant="permanent"
|
||||||
<ListItemText primary={text} />
|
anchor="left"
|
||||||
</ListItemButton>
|
>
|
||||||
</ListItem>
|
<List>
|
||||||
))}
|
{NAV_LINKS.map(({text, to, icon: Icon}) => (
|
||||||
</List>
|
<ListItem key={to} disablePadding>
|
||||||
<Divider sx={{ mt: 'auto' }} />
|
<ListItemButton component={Link} to={to}>
|
||||||
<List>
|
<ListItemIcon>
|
||||||
{BOTTOM_LINKS.map(({ text, icon: Icon, href }) => (
|
<Icon/>
|
||||||
<ListItem key={text} disablePadding>
|
</ListItemIcon>
|
||||||
<ListItemButton href={href}>
|
<ListItemText primary={text}/>
|
||||||
<ListItemIcon>
|
</ListItemButton>
|
||||||
<Icon />
|
</ListItem>
|
||||||
</ListItemIcon>
|
))}
|
||||||
<ListItemText primary={text} />
|
</List>
|
||||||
</ListItemButton>
|
<Divider sx={{mt: 'auto'}}/>
|
||||||
</ListItem>
|
<List>
|
||||||
))}
|
{BOTTOM_LINKS.map(({text, icon: Icon, href, onClick}) => (
|
||||||
</List>
|
<ListItem key={text} disablePadding>
|
||||||
</Drawer>
|
<ListItemButton type={"submit"} onClick={onClick} href={href}>
|
||||||
<Box
|
<ListItemIcon>
|
||||||
component="main"
|
<Icon/>
|
||||||
sx={{
|
</ListItemIcon>
|
||||||
position: "absolute",
|
<ListItemText primary={text}/>
|
||||||
top: 0,
|
</ListItemButton>
|
||||||
flexGrow: 1,
|
</ListItem>
|
||||||
bgcolor: 'background.default',
|
))}
|
||||||
ml: `${DRAWER_WIDTH}px`,
|
{!isLoggedIn() && <ListItem key="login-btn" disablePadding>
|
||||||
p: 3,
|
<ListItemButton component="button" href="/oauth2/authorization/authentik">
|
||||||
}}
|
<ListItemIcon>
|
||||||
>
|
<LoginIcon/>
|
||||||
{children}
|
</ListItemIcon>
|
||||||
</Box>
|
<ListItemText primary="Login"/>
|
||||||
<Toaster
|
</ListItemButton>
|
||||||
toastOptions={{
|
</ListItem>}
|
||||||
success: {
|
{isLoggedIn() && <form method="POST" action="/logout">
|
||||||
style: {
|
<ListItem key="logout-btn" disablePadding>
|
||||||
background: '#dad7cd',
|
<ListItemButton component="button" type="submit">
|
||||||
},
|
<ListItemIcon>
|
||||||
},
|
<LogoutIcon/>
|
||||||
error: {
|
</ListItemIcon>
|
||||||
style: {
|
<ListItemText primary="Logout"/>
|
||||||
background: '#ff8fab',
|
</ListItemButton>
|
||||||
},
|
</ListItem>
|
||||||
},
|
</form>}
|
||||||
}}
|
</List>
|
||||||
/>
|
</Drawer>
|
||||||
</ ThemeProvider>
|
<Box
|
||||||
);
|
component="main"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
bgcolor: 'background.default',
|
||||||
|
ml: `${DRAWER_WIDTH}px`,
|
||||||
|
p: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Toaster
|
||||||
|
toastOptions={{
|
||||||
|
success: {
|
||||||
|
style: {
|
||||||
|
background: '#dad7cd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
style: {
|
||||||
|
background: '#ff8fab',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ ThemeProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import { AppBar, Stack } from "@mui/material";
|
import Button from "@mui/material/Button";
|
||||||
|
import { Publish as ImportExportIcon } from "@mui/icons-material";
|
||||||
|
|
||||||
export default function TransactionsPage() {
|
export default function TransactionsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBar position="static">Transactions</AppBar>
|
<Button variant="contained" onClick={() => {
|
||||||
|
fetch("/api/user-info", {
|
||||||
|
method: "POST"
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
|
console.log(resp)
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<ImportExportIcon /> Import
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -16,8 +16,5 @@ export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
|
||||||
"/api": "http://localhost:8080"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue