mirror of
https://github.com/mvvasilev/personal-finances.git
synced 2025-04-18 21:59: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
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -42,4 +42,5 @@ bin/
|
|||
.DS_Store
|
||||
|
||||
### .env files ###
|
||||
.env
|
||||
.env
|
||||
*.p12
|
|
@ -1,9 +1,17 @@
|
|||
AUTHENTIK_CLIENT_ID= authentik oauth2 client id
|
||||
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
|
||||
API_URI= http://localhost:8081
|
||||
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_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.RedisSerializer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
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.ServerAuthenticationEntryPoint;
|
||||
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.web.server.WebSession;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@EnableRedisWebSession
|
||||
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}")
|
||||
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;
|
||||
|
||||
@Bean
|
||||
|
@ -41,6 +53,23 @@ public class SecurityConfiguration implements BeanClassLoaderAware {
|
|||
c.pathMatchers("/api/**").authenticated();
|
||||
})
|
||||
.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);
|
||||
})
|
||||
.logout(c -> {
|
||||
|
@ -50,6 +79,17 @@ public class SecurityConfiguration implements BeanClassLoaderAware {
|
|||
response.setStatusCode(HttpStatus.SEE_OTHER);
|
||||
response.getHeaders().set("Location", backChannelLogoutUrl);
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ spring:
|
|||
redirect-uri: "{baseUrl}/login/oauth2/code/authentik"
|
||||
provider:
|
||||
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}
|
||||
cloud:
|
||||
gateway:
|
||||
|
@ -26,7 +26,7 @@ spring:
|
|||
routes:
|
||||
- id: api
|
||||
uri: ${API_URI}
|
||||
order: 0
|
||||
order: 1
|
||||
predicates:
|
||||
- Path=/api/**
|
||||
filters:
|
||||
|
@ -35,4 +35,19 @@ spring:
|
|||
order: 10
|
||||
uri: ${FRONTEND_URI}
|
||||
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_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.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Optional;
|
||||
|
@ -26,7 +27,7 @@ public class ApiController {
|
|||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/api/user-info")
|
||||
@PostMapping("/api/user-info")
|
||||
public ResponseEntity<Authentication> userInfo(JwtAuthenticationToken authenticationToken) {
|
||||
logger.info(authenticationToken.getToken().getClaimAsString(JwtClaimNames.SUB));
|
||||
return ResponseEntity.of(Optional.of(SecurityContextHolder.getContext().getAuthentication()));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {Link} from 'react-router-dom';
|
||||
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 Drawer from '@mui/material/Drawer';
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
@ -10,92 +10,128 @@ import ListItem from '@mui/material/ListItem';
|
|||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { Home as HomeIcon } from '@mui/icons-material';
|
||||
import { Receipt as TransactionsIcon } from '@mui/icons-material';
|
||||
import { Logout as LogoutIcon } from '@mui/icons-material';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import {Home as HomeIcon} from '@mui/icons-material';
|
||||
import {Receipt as TransactionsIcon} from '@mui/icons-material';
|
||||
import {Logout as LogoutIcon} from '@mui/icons-material';
|
||||
import {Login as LoginIcon} from "@mui/icons-material";
|
||||
import {Toaster} from 'react-hot-toast';
|
||||
import theme from '../components/ThemeRegistry/theme';
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ text: 'Home', to: '/', icon: HomeIcon },
|
||||
{ text: 'Transactions', to: '/transactions', icon: TransactionsIcon },
|
||||
{text: 'Home', to: '/', icon: HomeIcon},
|
||||
{text: 'Transactions', to: '/transactions', icon: TransactionsIcon},
|
||||
];
|
||||
|
||||
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 }) {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Drawer
|
||||
sx={{
|
||||
width: DRAWER_WIDTH,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width: DRAWER_WIDTH,
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
}}
|
||||
variant="permanent"
|
||||
anchor="left"
|
||||
>
|
||||
<List>
|
||||
{NAV_LINKS.map(({ text, to, icon: Icon }) => (
|
||||
<ListItem key={to} disablePadding>
|
||||
<ListItemButton component={Link} to={to}>
|
||||
<ListItemIcon>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={text} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Divider sx={{ mt: 'auto' }} />
|
||||
<List>
|
||||
{BOTTOM_LINKS.map(({ text, icon: Icon, href }) => (
|
||||
<ListItem key={text} disablePadding>
|
||||
<ListItemButton href={href}>
|
||||
<ListItemIcon>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={text} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Drawer>
|
||||
<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>
|
||||
);
|
||||
function getCookie(key) {
|
||||
var b = document.cookie.match("(^|;)\\s*" + key + "\\s*=\\s*([^;]+)");
|
||||
return b ? b.pop() : "";
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return getCookie("isLoggedIn") === "true";
|
||||
}
|
||||
|
||||
export default function RootLayout({children}) {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline/>
|
||||
<Drawer
|
||||
sx={{
|
||||
width: DRAWER_WIDTH,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width: DRAWER_WIDTH,
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
}}
|
||||
variant="permanent"
|
||||
anchor="left"
|
||||
>
|
||||
<List>
|
||||
{NAV_LINKS.map(({text, to, icon: Icon}) => (
|
||||
<ListItem key={to} disablePadding>
|
||||
<ListItemButton component={Link} to={to}>
|
||||
<ListItemIcon>
|
||||
<Icon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={text}/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Divider sx={{mt: 'auto'}}/>
|
||||
<List>
|
||||
{BOTTOM_LINKS.map(({text, icon: Icon, href, onClick}) => (
|
||||
<ListItem key={text} disablePadding>
|
||||
<ListItemButton type={"submit"} onClick={onClick} href={href}>
|
||||
<ListItemIcon>
|
||||
<Icon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={text}/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{!isLoggedIn() && <ListItem key="login-btn" disablePadding>
|
||||
<ListItemButton component="button" href="/oauth2/authorization/authentik">
|
||||
<ListItemIcon>
|
||||
<LoginIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Login"/>
|
||||
</ListItemButton>
|
||||
</ListItem>}
|
||||
{isLoggedIn() && <form method="POST" action="/logout">
|
||||
<ListItem key="logout-btn" disablePadding>
|
||||
<ListItemButton component="button" type="submit">
|
||||
<ListItemIcon>
|
||||
<LogoutIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Logout"/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</form>}
|
||||
</List>
|
||||
</Drawer>
|
||||
<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() {
|
||||
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: {
|
||||
host: '127.0.0.1',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue