Add security scheme to controller for client generation
All checks were successful
continuous-integration/drone/push Build is passing

Clients (at least javascript client) generated from the OpenAPI spec do add the
bearer token to the request without specifying the bearer requirement.
This commit is contained in:
Armin Friedl 2020-07-13 16:20:13 +02:00
parent 39fd416b4a
commit bcfbf349cd
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
7 changed files with 105 additions and 67 deletions

View file

@ -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.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse; 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 io.swagger.v3.oas.annotations.tags.Tag;
import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.service.ArtifactService; import net.friedl.fling.service.ArtifactService;
@ -28,6 +29,7 @@ import net.friedl.fling.service.archive.ArchiveService;
@RestController @RestController
@RequestMapping("/api/artifacts") @RequestMapping("/api/artifacts")
@Tag(name = "artifact", description = "Operations on /api/artifacts") @Tag(name = "artifact", description = "Operations on /api/artifacts")
@SecurityRequirement(name = "bearer")
@Validated @Validated
public class ArtifactController { public class ArtifactController {

View file

@ -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.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; 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 io.swagger.v3.oas.annotations.tags.Tag;
import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.dto.FlingDto;
@ -32,6 +33,7 @@ import net.friedl.fling.service.archive.ArchiveService;
@RestController @RestController
@RequestMapping("/api/fling") @RequestMapping("/api/fling")
@Tag(name = "fling", description = "Operations on /api/fling") @Tag(name = "fling", description = "Operations on /api/fling")
@SecurityRequirement(name = "bearer")
public class FlingController { public class FlingController {
private FlingService flingService; private FlingService flingService;

View file

@ -3,7 +3,7 @@ import React from 'react';
import {Switch, Route, Redirect} from "react-router-dom"; 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 Login from './components/admin/Login';
import FlingAdmin from './components/admin/FlingAdmin'; import FlingAdmin from './components/admin/FlingAdmin';
@ -13,53 +13,64 @@ import FlingUser from './components/user/FlingUser';
import LandingPage from './components/LandingPage'; import LandingPage from './components/LandingPage';
export default () => { export default () => {
return ( return (
<Switch> <Switch>
<Route exact path="/" component={LandingPage} /> <Route exact path="/" component={LandingPage} />
<Route exact path="/admin/login" component={Login} /> <Route exact path="/admin/login" component={Login} />
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute> <OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute> <OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute>
<Route exact path="/unlock" component={Unlock} /> <Route exact path="/unlock" component={Unlock} />
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute> <UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
<Route match="*">Not implemented</Route> <Route match="*">Not implemented</Route>
</Switch> </Switch>
); );
} }
// A wrapper for <Route> that redirects to the login /*
// screen if you're not yet authenticated. * A wrapper for <Route> 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 }) { function OwnerRoute({ children, ...rest }) {
return ( log.info(`Routing request for ${rest['path']}`);
<Route return (
{...rest} <Route
render={({ location }) => { {...rest}
log.info(request.defaults); render={({ location }) => {
if(isOwner()) { return children; } if (jwt.hasSubject("admin")) { return children; }
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; } else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; }
}} }}
/> />
); );
} }
// A wrapper for <Route> that redirects to the unlock /* A wrapper for <Route> that redirects to the unlock screen if no authorized token
// screen if the fling is protected * 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 }) { function UserRoute({ children, ...rest }) {
return ( log.debug(`Routing request for ${rest['path']}`);
<Route return (
{...rest} <Route
render={({ match, location }) => { {...rest}
log.info(request.defaults); render={({ match, location }) => {
log.info(match); let state = {from: location, shareId: match.params.shareId};
log.info(location); let authorized = jwt.hasSubject("admin") || (jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId']));
let x = {from: location, shareId: match.params.shareId};
if(isOwner()) { return children; } if (authorized) { return children; }
else if(isUser(match.params.shareId)) { return children; } else { return <Redirect to={ {pathname: "/unlock", state: state} } />; }
else { return <Redirect to={ {pathname: "/unlock", state: x} } />; } }}
}} />
/> );
);
} }

View file

@ -1,37 +1,34 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState, useEffect} from 'react'; import React, {useState, useEffect} from 'react';
import {flingClient} from '../../util/flingclient'; import {FlingClient} from '../../util/fc';
import FlingTile from './FlingTile'; import FlingTile from './FlingTile';
export default function FlingList(props) { export default function FlingList(props) {
const [flings, setFlings] = useState([]); const [flings, setFlings] = useState([]);
useEffect(() => { (async () => { useEffect(() => {
let flings = await flingClient.getFlings(); let flingClient = new FlingClient();
flingClient.getFlings()
.then(flings => {
let newFlings = []; let newFlings = [];
for (let fling of flings) { for (let fling of flings) {
let flingTile = <FlingTile fling={fling} let flingTile = <FlingTile fling={fling} key={fling.id} />;
key={fling.id} newFlings.push(flingTile);
refreshFlingListFn={refreshFlingList} />;
newFlings.push(flingTile);
} }
setFlings(newFlings); setFlings(newFlings);
})(); } , []); }).catch(log.error);
}, []);
return( return(
<div className="panel"> <div className="panel">
{log.info(`Got active fling: ${props.activeFling}`)} {log.info(`Got active fling: ${props.activeFling}`)}
<div className="panel-header p-2"> <div className="panel-header p-2">
<h5>My Flings</h5> <h5>My Flings</h5>
</div> </div>
<div className="panel-body p-0"> <div className="panel-body p-0">
{flings} {flings}
</div> </div>
</div> </div>
); );
async function refreshFlingList() {
}
} }

View file

@ -51,8 +51,8 @@ export default function Login() {
authClient.authenticateOwner(opt) authClient.authenticateOwner(opt)
.then(response => { .then(response => {
log.info("Login successful"); log.info("Login successful");
sessionStorage.setItem['token'] = response; sessionStorage.setItem('token', response);
log.info("Returning back to", from); log.debug("Returning back to", from);
history.replace(from); history.replace(from);
}).catch(log.error); }).catch(log.error);
}; };

View file

@ -4,7 +4,7 @@ let clientConfig = (token) => {
let config = new fc.ApiClient(); let config = new fc.ApiClient();
config.basePath = process.env.REACT_APP_API.replace(/\/+$/, ''); 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; } if(token) { config.authentications['bearer'].accessToken = token; }
return config; return config;

26
web/fling/src/util/jwt.js Normal file
View file

@ -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;