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.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 {

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

View file

@ -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 (
<Switch>
<Route exact path="/" component={LandingPage} />
return (
<Switch>
<Route exact path="/" component={LandingPage} />
<Route exact path="/admin/login" component={Login} />
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute>
<Route exact path="/admin/login" component={Login} />
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute>
<Route exact path="/unlock" component={Unlock} />
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
<Route exact path="/unlock" component={Unlock} />
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
<Route match="*">Not implemented</Route>
</Switch>
);
<Route match="*">Not implemented</Route>
</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 }) {
return (
<Route
{...rest}
render={({ location }) => {
log.info(request.defaults);
if(isOwner()) { return children; }
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; }
}}
/>
);
log.info(`Routing request for ${rest['path']}`);
return (
<Route
{...rest}
render={({ location }) => {
if (jwt.hasSubject("admin")) { return children; }
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; }
}}
/>
);
}
// A wrapper for <Route> that redirects to the unlock
// screen if the fling is protected
/* A wrapper for <Route> 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 (
<Route
{...rest}
render={({ match, location }) => {
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 (
<Route
{...rest}
render={({ match, location }) => {
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 <Redirect to={ {pathname: "/unlock", state: x} } />; }
}}
/>
);
if (authorized) { return children; }
else { return <Redirect to={ {pathname: "/unlock", state: state} } />; }
}}
/>
);
}

View file

@ -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 = <FlingTile fling={fling}
key={fling.id}
refreshFlingListFn={refreshFlingList} />;
newFlings.push(flingTile);
let flingTile = <FlingTile fling={fling} key={fling.id} />;
newFlings.push(flingTile);
}
setFlings(newFlings);
})(); } , []);
}).catch(log.error);
}, []);
return(
<div className="panel">
{log.info(`Got active fling: ${props.activeFling}`)}
<div className="panel-header p-2">
<h5>My Flings</h5>
</div>
<div className="panel-body p-0">
{flings}
</div>
</div>
);
async function refreshFlingList() {
}
return(
<div className="panel">
{log.info(`Got active fling: ${props.activeFling}`)}
<div className="panel-header p-2">
<h5>My Flings</h5>
</div>
<div className="panel-body p-0">
{flings}
</div>
</div>
);
}

View file

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

View file

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

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;