From bcfbf349cde81bb6a100bf2e5e6b08780630da9f Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Mon, 13 Jul 2020 16:20:13 +0200 Subject: [PATCH] Add security scheme to controller for client generation Clients (at least javascript client) generated from the OpenAPI spec do add the bearer token to the request without specifying the bearer requirement. --- .../fling/controller/ArtifactController.java | 2 + .../fling/controller/FlingController.java | 2 + web/fling/src/App.jsx | 91 +++++++++++-------- web/fling/src/components/admin/FlingList.jsx | 45 +++++---- web/fling/src/components/admin/Login.jsx | 4 +- web/fling/src/util/fc.js | 2 +- web/fling/src/util/jwt.js | 26 ++++++ 7 files changed, 105 insertions(+), 67 deletions(-) create mode 100644 web/fling/src/util/jwt.js diff --git a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java index 7dcde8e..e6e126f 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.service.ArtifactService; @@ -28,6 +29,7 @@ import net.friedl.fling.service.archive.ArchiveService; @RestController @RequestMapping("/api/artifacts") @Tag(name = "artifact", description = "Operations on /api/artifacts") +@SecurityRequirement(name = "bearer") @Validated public class ArtifactController { diff --git a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java index ba9a11c..3037aa2 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java @@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.dto.FlingDto; @@ -32,6 +33,7 @@ import net.friedl.fling.service.archive.ArchiveService; @RestController @RequestMapping("/api/fling") @Tag(name = "fling", description = "Operations on /api/fling") +@SecurityRequirement(name = "bearer") public class FlingController { private FlingService flingService; diff --git a/web/fling/src/App.jsx b/web/fling/src/App.jsx index d3dcb83..5cfcb31 100644 --- a/web/fling/src/App.jsx +++ b/web/fling/src/App.jsx @@ -3,7 +3,7 @@ import React from 'react'; import {Switch, Route, Redirect} from "react-router-dom"; -import request, {isOwner, isUser} from './util/request'; +import jwt from './util/jwt.js'; import Login from './components/admin/Login'; import FlingAdmin from './components/admin/FlingAdmin'; @@ -13,53 +13,64 @@ import FlingUser from './components/user/FlingUser'; import LandingPage from './components/LandingPage'; export default () => { - return ( - - + return ( + + - - - + + + - - + + - Not implemented - - ); + Not implemented + + ); } -// A wrapper for that redirects to the login -// screen if you're not yet authenticated. +/* + * A wrapper for that redirects to the login screen if no admin + * authentication token was found. + * + * Note that the token check is purely client-side. It provides no actual + * protection! It is hence possible to reach the admin site with some small + * amount of trickery. Without a valid token no meaningful actions are possible + * on the admin page though. + */ function OwnerRoute({ children, ...rest }) { - return ( - { - log.info(request.defaults); - if(isOwner()) { return children; } - else { return ; } - }} - /> - ); + log.info(`Routing request for ${rest['path']}`); + return ( + { + if (jwt.hasSubject("admin")) { return children; } + else { return ; } + }} + /> + ); } -// A wrapper for that redirects to the unlock -// screen if the fling is protected +/* A wrapper for that redirects to the unlock screen if no authorized token + * was found. + * + * Note that the token check is purely client-side. It provides no actual + * protection! It is hence possible to reach the target site with some small + * amount of trickery. Without a valid token, no meaningful actions are possible + * on the target page though - this must be checked server side. + */ function UserRoute({ children, ...rest }) { - return ( - { - log.info(request.defaults); - log.info(match); - log.info(location); - let x = {from: location, shareId: match.params.shareId}; + log.debug(`Routing request for ${rest['path']}`); + return ( + { + let state = {from: location, shareId: match.params.shareId}; + let authorized = jwt.hasSubject("admin") || (jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId'])); - if(isOwner()) { return children; } - else if(isUser(match.params.shareId)) { return children; } - else { return ; } - }} - /> - ); + if (authorized) { return children; } + else { return ; } + }} + /> + ); } diff --git a/web/fling/src/components/admin/FlingList.jsx b/web/fling/src/components/admin/FlingList.jsx index 3b501a6..99da35d 100644 --- a/web/fling/src/components/admin/FlingList.jsx +++ b/web/fling/src/components/admin/FlingList.jsx @@ -1,37 +1,34 @@ import log from 'loglevel'; import React, {useState, useEffect} from 'react'; -import {flingClient} from '../../util/flingclient'; +import {FlingClient} from '../../util/fc'; import FlingTile from './FlingTile'; export default function FlingList(props) { - const [flings, setFlings] = useState([]); - useEffect(() => { (async () => { - let flings = await flingClient.getFlings(); + const [flings, setFlings] = useState([]); + useEffect(() => { + let flingClient = new FlingClient(); + flingClient.getFlings() + .then(flings => { let newFlings = []; - for (let fling of flings) { - let flingTile = ; - newFlings.push(flingTile); + let flingTile = ; + newFlings.push(flingTile); } setFlings(newFlings); - })(); } , []); + }).catch(log.error); + }, []); - return( -
- {log.info(`Got active fling: ${props.activeFling}`)} -
-
My Flings
-
-
- {flings} -
-
- ); - - async function refreshFlingList() { - } + return( +
+ {log.info(`Got active fling: ${props.activeFling}`)} +
+
My Flings
+
+
+ {flings} +
+
+ ); } diff --git a/web/fling/src/components/admin/Login.jsx b/web/fling/src/components/admin/Login.jsx index b56f599..3599e22 100644 --- a/web/fling/src/components/admin/Login.jsx +++ b/web/fling/src/components/admin/Login.jsx @@ -51,8 +51,8 @@ export default function Login() { authClient.authenticateOwner(opt) .then(response => { log.info("Login successful"); - sessionStorage.setItem['token'] = response; - log.info("Returning back to", from); + sessionStorage.setItem('token', response); + log.debug("Returning back to", from); history.replace(from); }).catch(log.error); }; diff --git a/web/fling/src/util/fc.js b/web/fling/src/util/fc.js index b901f7a..11445d7 100644 --- a/web/fling/src/util/fc.js +++ b/web/fling/src/util/fc.js @@ -4,7 +4,7 @@ let clientConfig = (token) => { let config = new fc.ApiClient(); config.basePath = process.env.REACT_APP_API.replace(/\/+$/, ''); - token = token || sessionStorage.getItem['token']; + token = token || sessionStorage.getItem('token'); if(token) { config.authentications['bearer'].accessToken = token; } return config; diff --git a/web/fling/src/util/jwt.js b/web/fling/src/util/jwt.js new file mode 100644 index 0000000..696d13e --- /dev/null +++ b/web/fling/src/util/jwt.js @@ -0,0 +1,26 @@ +/** + * Utilities for working with JWT tokens + */ +import jwtDecode from 'jwt-decode'; + +let jwt = { + /** + * Check the session store token for an arbitrary claim + */ + hasClaim: function (name, value) { + if(!sessionStorage.getItem('token')) return false; + let tokenPayload = jwtDecode(sessionStorage.getItem('token')); + return tokenPayload[name] === value; + }, + + /** + * Check the session store token for a subject + */ + hasSubject: function (value) { + if(!sessionStorage.getItem('token')) return false; + let tokenPayload = jwtDecode(sessionStorage.getItem('token')); + return tokenPayload['sub'] === value; + } +}; + +export default jwt;