[release] v0.6.0-unstable2

This commit is contained in:
Yann Stepienik 2023-06-05 22:00:21 +01:00
parent ebe8423a2d
commit 1970f14eac
19 changed files with 2067 additions and 7937 deletions

View file

@ -0,0 +1,13 @@
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, SafetyOutlined, UpOutlined } from "@ant-design/icons";
import { Card, Chip, Stack, Tooltip } from "@mui/material";
import { useState } from "react";
import { useTheme } from '@mui/material/styles';
export const DeleteButton = ({onDelete}) => {
const [confirmDelete, setConfirmDelete] = useState(false);
return (<>
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => onDelete(event)}/>)}
</>);
}

View file

@ -45,7 +45,7 @@ export const RouteMode = ({route}) => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
let c = routeImages[route.Mode.toUpperCase()];
return <>
return c ? <>
<Chip
icon={<span>{c.icon}</span>}
label={c.label}
@ -56,7 +56,7 @@ export const RouteMode = ({route}) => {
alignItems: "right",
}}
></Chip>
</>
</> : <></>;
}
export const RouteSecurity = ({route}) => {

View file

@ -1,5 +1,5 @@
// assets
import { ProfileOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
import { ProfileOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
// icons
const icons = {
@ -36,6 +36,13 @@ const pages = {
url: '/ui/config-users',
icon: icons.ProfileOutlined,
},
{
id: 'openid',
title: 'OpenID',
type: 'item',
url: '/ui/openid-manage',
icon: PicLeftOutlined,
},
{
id: 'config',
title: 'Configuration',

View file

@ -7,10 +7,15 @@ import { Grid, Stack, Typography } from '@mui/material';
import AuthLogin from './auth-forms/AuthLogin';
import AuthWrapper from './AuthWrapper';
const selfport = new URL(window.location.href).port
const selfprotocol = new URL(window.location.href).protocol + "//"
const selfHostname = selfprotocol + (new URL(window.location.href).hostname) + (selfport ? ":" + selfport : "")
// ================================|| LOGIN ||================================ //
const Login = () => (
<AuthWrapper>
<link rel="openid2.provider openid.server" href={selfHostname + "/oauth2/auth"} />
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>

View file

@ -22,14 +22,22 @@ const OpenID = () => {
const entireSearch = searchParams.toString()
const [checkedScopes, setCheckedScopes] = useState(["openid"])
let icon;
// get hostname from redirect_uri with port
const port = new URL(redirect_uri).port
const protocol = new URL(redirect_uri).protocol + "//"
const appHostname = protocol + (new URL(redirect_uri).hostname) + (port ? ":" + port : "")
const icon = getFaviconURL({
Mode: 'PROXY',
Target: appHostname
});
let port, protocol, appHostname;
try {
port = new URL(redirect_uri).port
protocol = new URL(redirect_uri).protocol + "//"
appHostname = protocol + (new URL(redirect_uri).hostname) + (port ? ":" + port : "")
icon = getFaviconURL({
Mode: 'PROXY',
Target: appHostname
});
} catch (e) {
icon = getFaviconURL();
}
const selfport = new URL(window.location.href).port
const selfprotocol = new URL(window.location.href).protocol + "//"
@ -55,7 +63,7 @@ const OpenID = () => {
}}>
<img src={icon} alt={'icon'} width="64px" />
<div>
You are logging in to <b>{client_id}</b>. <br />
You are about to login into <b>{client_id}</b>. <br />
Check which permissions you are giving to this application. <br />
</div>
</Stack>
@ -84,7 +92,7 @@ const OpenID = () => {
opacity: '0.8',
fontStyle: 'italic',
}}>
You will be redirected to <b>{appHostname}</b> after login. <br />
You will be redirected to <b>{redirect_uri}</b> after login. <br />
</div>
<LoadingButton

View file

@ -0,0 +1,99 @@
// material-ui
import { AppstoreAddOutlined, PlusCircleOutlined, ReloadOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material';
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
import { Stack } from '@mui/system';
import { useEffect, useState } from 'react';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import * as API from '../../api';
import IsLoggedIn from '../../isLoggedIn';
import RestartModal from '../config/users/restart';
import RouteManagement from '../config/routes/routeman';
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes';
import HostChip from '../../components/hostChip';
import { Formik, useFormik } from 'formik';
import * as yup from 'yup';
const OpenIdEditModal = ({ clientId, openNewModal, setOpenNewModal, config, onSubmit }) => {
const [openRestartModal, setOpenRestartModal] = useState(false);
const [submitErrors, setSubmitErrors] = useState([]);
const [newRoute, setNewRoute] = useState(null);
function addRoute() {
return API.config.addRoute(newRoute).then((res) => {
setOpenNewModal(false);
setOpenRestartModal(true);
});
}
const clientConfig = Object.values(config.OpenIDClients).find((c) => c.id === clientId);
return <>
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
<Formik
initialValues={{
id: clientConfig ? clientConfig.id : '',
redirect: clientConfig ? clientConfig.redirect : '',
}}
validationSchema={yup.object({
id: yup.string().required('Required'),
redirect: yup.string().required('Required'),
})}
onSubmit={(values) => {
onSubmit && onSubmit(values);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<DialogTitle>{clientId ? clientId : "New client"}</DialogTitle>
{openNewModal && <>
<DialogContent>
<DialogContentText>
<Stack spacing={2} style={{ marginTop: '10px', width: '500px', maxWidth: '100%' }}>
<TextField
fullWidth
id="id"
name="id"
label="ID"
value={formik.values.id}
onChange={formik.handleChange}
error={formik.touched.id && Boolean(formik.errors.id)}
helperText={formik.touched.id && formik.errors.id}
/>
<TextField
fullWidth
id="redirect"
name="redirect"
label="Redirect"
value={formik.values.redirect}
onChange={formik.handleChange}
error={formik.touched.redirect && Boolean(formik.errors.redirect)}
helperText={formik.touched.redirect && formik.errors.redirect}
/>
</Stack>
</DialogContentText>
</DialogContent>
<DialogActions>
{submitErrors && submitErrors.length > 0 && <Stack spacing={2} direction={"column"}>
<Alert severity="error">{submitErrors.map((err) => {
return <div>{err}</div>
})}</Alert>
</Stack>}
<Button onClick={() => setOpenNewModal(false)}>Cancel</Button>
<Button color="primary" variant="contained" type="submit" onClick={() => {
formik.handleSubmit();
}}>{clientId ? "Edit" : "Create"}</Button>
</DialogActions>
</>}
</form>
)}
</Formik>
</Dialog>
</>;
}
export default OpenIdEditModal;

View file

@ -0,0 +1,236 @@
import * as React from 'react';
import IsLoggedIn from '../../isLoggedIn';
import * as API from '../../api';
import MainCard from '../../components/MainCard';
import { Formik, Field } from 'formik';
import * as Yup from 'yup';
import { useTheme } from '@mui/material/styles';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined, SyncOutlined, UserOutlined, KeyOutlined, ArrowRightOutlined } from '@ant-design/icons';
import {
Alert,
Button,
Checkbox,
Stack,
Typography,
FormHelperText,
Chip,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
IconButton,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../components/@extended/AnimateButton';
import RestartModal from '../config/users/restart';
import RouteManagement from '../config/routes/routeman';
import { map } from 'lodash';
import { getFaviconURL, sanitizeRoute, ValidateRoute } from '../../utils/routes';
import PrettyTableView from '../../components/tableView/prettyTableView';
import HostChip from '../../components/hostChip';
import { RouteActions, RouteMode, RouteSecurity } from '../../components/routeComponents';
import { useNavigate } from 'react-router';
import NewRouteCreate from '../config/routes/newRoute';
import { DeleteButton } from '../../components/delete';
import OpenIdEditModal from './openid-edit';
import bcrypt from 'bcryptjs';
const stickyButton = {
position: 'fixed',
bottom: '20px',
width: '300px',
boxShadow: '0px 0px 10px 0px rgba(0,0,0,0.50)',
right: '20px',
}
function shorten(test) {
if (test.length > 75) {
return test.substring(0, 75) + '...';
}
return test;
}
const OpenIdList = () => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
const [error, setError] = React.useState(null);
const [submitErrors, setSubmitErrors] = React.useState([]);
const [needSave, setNeedSave] = React.useState(false);
const [clientId, setClientId] = React.useState(false);
const [openNewModal, setOpenNewModal] = React.useState(false);
const [newSecret, setNewSecret] = React.useState(null);
function updateRoutes(routes) {
let con = {
...config,
OpenIDClients: routes,
};
setConfig(con);
return con;
}
function refresh() {
API.config.get().then((res) => {
setConfig(res.data);
});
}
function save(con) {
API.config.set(con).then((res) => {
refresh();
});
}
function getClientIcon(client) {
try {
let hostname = new URL(client.redirect).hostname;
let port = new URL(client.redirect).port;
let protocol = new URL(client.redirect).protocol + '//';
return getFaviconURL({
Mode: 'PROXY',
Target: protocol + hostname + (port ? ':' + port : ''),
})
} catch (e) {
return getFaviconURL()
}
}
function deleteClient(event, key) {
event.stopPropagation();
clients.splice(key, 1);
save(updateRoutes(clients));
return false;
}
React.useEffect(() => {
refresh();
}, []);
const generateNewSecret = (clientIdToUpdate) => {
let newSecret = Math.random().toString(36).substring(2, 24) + Math.random().toString(36).substring(2, 15);
let encryptedSecret = bcrypt.hashSync(newSecret, 10);
console.log(newSecret, encryptedSecret)
let index = clients.findIndex((r) => r.id === clientIdToUpdate);
clients[index].secret = encryptedSecret;
save(updateRoutes(clients));
setNewSecret(newSecret);
}
let clients = config && (config.OpenIDClients || []);
return <div style={{}}>
<IsLoggedIn />
<Stack direction="row" spacing={1} style={{ marginBottom: '20px' }}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button>&nbsp;&nbsp;
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
setClientId(null);
setOpenNewModal(true);
}}>Create</Button>
</Stack>
{config && <>
<OpenIdEditModal
clientId={clientId}
openNewModal={openNewModal}
setOpenNewModal={setOpenNewModal}
config={config}
onSubmit={(values) => {
if (clientId) {
let index = clients.findIndex((r) => r.id === clientId);
clients[index] = values;
} else {
clients.push(values);
}
save(updateRoutes(clients));
setOpenNewModal(false);
setClientId(null);
}}
/>
{newSecret && <Dialog open={newSecret} onClose={() => setNewSecret(false)}>
<DialogTitle>New Secret</DialogTitle>
<DialogContent>
<DialogContentText>
Secret has been updated. Please copy it now as it will not be shown again.
<Stack direction="row" spacing={2} style={{ marginTop: '10px', width: '100%', maxWidth: '100%' }}>
<div style={{overflowX: 'scroll', float: 'left', width: '100%', padding: '5px', background:'rgba(0,0,0,0.15)', whiteSpace: 'nowrap', wordBreak: 'keep-all', overflow: 'auto', fontStyle: 'italic'}}>
{newSecret}
</div>
<IconButton size="large" style={{float: 'left'}} aria-label="copy" onClick={
() => {
navigator.clipboard.writeText(newSecret);
}
}>
<CopyOutlined />
</IconButton>
</Stack>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setNewSecret(false)}>Close</Button>
</DialogActions>
</Dialog>}
{clients && <PrettyTableView
data={clients}
getKey={(r) => r.id}
onRowClick={(r) => {
setClientId(r.id);
setOpenNewModal(true);
}}
columns={[
{
title: '',
field: (r) => <img className="loading-image" alt="" src={getClientIcon(r)} width="64px" />,
style: {
textAlign: 'center',
width: '100px',
},
},
{
title: 'Client ID',
search: (r) => r.id,
style: {
textDecoration: 'inherit',
},
underline: true,
field: (r) => <>
<div style={{ display: 'inline-block', textDecoration: 'inherit', fontSize: '125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark }}>{r.id}</div><br />
</>
},
{
title: 'Redirect URI',
screenMin: 'sm',
search: (r) => r.redirect,
field: (r) => r.redirect,
},
{
title: '', clickable: true, field: (r, k) => <>
<Button variant="contained" color="primary" startIcon={<ArrowRightOutlined />} onClick={() => {
generateNewSecret(r.id)
}}>Reset Secret</Button>&nbsp;&nbsp;
<DeleteButton onDelete={(event) => deleteClient(event, k)} />
</>,
},
]}
/>}
{
!clients && <div style={{ textAlign: 'center' }}>
<CircularProgress />
</div>
}
</>}
</div>;
}
export default OpenIdList;

View file

@ -14,6 +14,7 @@ import HomePage from '../pages/home';
import ContainerIndex from '../pages/servapps/containers';
import NewDockerService from '../pages/servapps/containers/newService';
import NewDockerServiceForm from '../pages/servapps/containers/newServiceForm';
import OpenIdList from '../pages/openid/openid-list';
// render - dashboard
@ -81,6 +82,10 @@ const MainRoutes = {
path: '/ui/servapps/containers/:containerName',
element: <ContainerIndex />,
},
{
path: '/ui/openid-manage',
element: <OpenIdList />,
}
]
};

9238
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.6.0-unstable",
"version": "0.6.0-unstable2",
"description": "",
"main": "test-server.js",
"bugs": {
@ -22,6 +22,8 @@
"@testing-library/user-event": "^14.4.3",
"@vitejs/plugin-react": "^3.1.0",
"apexcharts": "^3.35.5",
"bcryptjs": "^2.4.3",
"browserslist": "^4.21.7",
"formik": "^2.2.9",
"framer-motion": "^7.3.6",
"history": "^5.3.0",
@ -55,7 +57,7 @@
"scripts": {
"client": "vite",
"client-build": "vite build --base=/ui/",
"start": "env COSMOS_HOSTNAME=localhost CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
"start": "env CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
"build": "sh build.sh",
"dev": "npm run build && npm run start",
"dockerdevbuild": "sh build.sh && docker build -f dockerfile.local --tag cosmos-dev .",

View file

@ -1,12 +1,15 @@
package authorizationserver
import (
"crypto/rand"
"math/rand"
"crypto/rsa"
"github.com/ory/fosite"
"time"
"net/http"
"os"
"crypto/sha256"
"encoding/binary"
"github.com/ory/fosite/compose"
"github.com/ory/fosite/handler/openid"
@ -18,6 +21,53 @@ import (
"github.com/gorilla/mux"
)
var oauth2 fosite.OAuth2Provider
var AuthPrivateKey *rsa.PrivateKey
func Init() {
config := utils.ReadConfigFromFile()
authKey := config.HTTPConfig.AuthPrivateKey
secret := []byte(authKey[32:64])
foconfig := &fosite.Config{
AccessTokenLifespan: time.Minute * 30,
GlobalSecret: secret,
}
store := storage.NewMemoryStore()
// loop config.OpenIDClients
for _, client := range config.OpenIDClients {
utils.Log("Registering OpenID client: " + client.ID)
// register client
store.Clients[client.ID] = &fosite.DefaultClient{
ID: client.ID,
Secret: []byte(client.Secret),
RedirectURIs: []string{client.Redirect},
Scopes: []string{"openid", "email"},
ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"},
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
}
}
// Compute a hash of your authKey to use as a seed for deterministic RSA key generation
seedHash := sha256.Sum256([]byte(authKey))
seed := int64(binary.BigEndian.Uint64(seedHash[:]))
// Now you can create a deterministic source of randomness
deterministicReader := rand.New(rand.NewSource(seed))
// privateKey is used to sign JWT tokens. The default strategy uses RS256 (RSA Signature with SHA-256)
AuthPrivateKey, _ = rsa.GenerateKey(deterministicReader, 2048)
// Build a fosite instance with all OAuth2 and OpenID Connect handlers enabled, plugging in our configurations as specified above.
oauth2 = compose.ComposeAllEnabled(foconfig, store, AuthPrivateKey)
utils.Log("OpenID server initialized")
}
func RegisterHandlers(wellKnown *mux.Router, userRouter *mux.Router, serverRouter *mux.Router) {
// Set up oauth2 endpoints. You could also use gorilla/mux or any other router.
userRouter.HandleFunc("/auth", authEndpoint)
@ -33,94 +83,8 @@ func RegisterHandlers(wellKnown *mux.Router, userRouter *mux.Router, serverRoute
// public endpoints
wellKnown.HandleFunc("/openid-configuration", discoverEndpoint)
wellKnown.HandleFunc("/jwks.json", jwksEndpoint)
store.Clients["gitea"] = &fosite.DefaultClient{
ID: "gitea",
Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar"
RedirectURIs: []string{"http://localhost:3000/user/oauth2/Cosmos/callback"},
Scopes: []string{"openid", "email", "profile"},
ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"},
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
}
store.Clients["my-client"] = &fosite.DefaultClient{
ID: "my-client",
Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar"
RotatedSecrets: [][]byte{[]byte(`$2y$10$X51gLxUQJ.hGw1epgHTE5u0bt64xM0COU7K9iAp.OFg8p2pUd.1zC`)}, // = "foobaz",
RedirectURIs: []string{"http://localhost:3846/callback"},
ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"},
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
Scopes: []string{"fosite", "openid", "photos", "offline"},
}
store.Clients["minio"] = &fosite.DefaultClient{
ID: "minio",
Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar"
RedirectURIs: []string{"http://localhost:9090/oauth_callback"},
Scopes: []string{"openid", "email", "profile", "groups"},
ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"},
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
}
}
// fosite requires four parameters for the server to get up and running:
// 1. config - for any enforcement you may desire, you can do this using `compose.Config`. You like PKCE, enforce it!
// 2. store - no auth service is generally useful unless it can remember clients and users.
// fosite is incredibly composable, and the store parameter enables you to build and BYODb (Bring Your Own Database)
// 3. secret - required for code, access and refresh token generation.
// 4. privateKey - required for id/jwt token generation.
var (
// Check the api documentation of `compose.Config` for further configuration options.
config = &fosite.Config{
AccessTokenLifespan: time.Minute * 30,
GlobalSecret: secret,
// ...
}
// This is the example storage that contains:
// * an OAuth2 Client with id "my-client" and secrets "foobar" and "foobaz" capable of all oauth2 and open id connect grant and response types.
// * a User for the resource owner password credentials grant type with username "peter" and password "secret".
//
// You will most likely replace this with your own logic once you set up a real world application.
store = storage.NewMemoryStore()
// This secret is used to sign authorize codes, access and refresh tokens.
// It has to be 32-bytes long for HMAC signing. This requirement can be configured via `compose.Config` above.
// In order to generate secure keys, the best thing to do is use crypto/rand:
//
// ```
// package main
//
// import (
// "crypto/rand"
// "encoding/hex"
// "fmt"
// )
//
// func main() {
// var secret = make([]byte, 32)
// _, err := rand.Read(secret)
// if err != nil {
// panic(err)
// }
// }
// ```
//
// If you require this to key to be stable, for example, when running multiple fosite servers, you can generate the
// 32byte random key as above and push it out to a base64 encoded string.
// This can then be injected and decoded as the `var secret []byte` on server start.
secret = []byte("some-cool-secret-that-is-32bytes")
// privateKey is used to sign JWT tokens. The default strategy uses RS256 (RSA Signature with SHA-256)
AuthPrivateKey, _ = rsa.GenerateKey(rand.Reader, 2048)
)
// Build a fosite instance with all OAuth2 and OpenID Connect handlers enabled, plugging in our configurations as specified above.
var oauth2 = compose.ComposeAllEnabled(config, store, AuthPrivateKey)
// A session is passed from the `/auth` to the `/token` endpoint. You probably want to store data like: "Who made the request",
// "What organization does that person belong to" and so on.
// For our use case, the session will meet the requirements imposed by JWT access tokens, HMAC access tokens and OpenID Connect
@ -132,6 +96,10 @@ var oauth2 = compose.ComposeAllEnabled(config, store, AuthPrivateKey)
//
// session = new(fosite.DefaultSession)
func newSession(user string, req *http.Request) *openid.DefaultSession {
if user != "" {
utils.Debug("Creating new session for user: " + user)
}
// get hostname from request
hostname := req.Host

View file

@ -1,7 +1,6 @@
package authorizationserver
import (
"log"
"net/http"
"github.com/azukaar/cosmos-server/src/utils"
)
@ -10,7 +9,6 @@ func authEndpoint(rw http.ResponseWriter, req *http.Request) {
// This context will be passed to all methods.
ctx := req.Context()
if utils.LoggedInOnly(rw, req) != nil {
return
}
@ -28,7 +26,7 @@ func authEndpoint(rw http.ResponseWriter, req *http.Request) {
// It will analyze the request and extract important information like scopes, response type and others.
ar, err := oauth2.NewAuthorizeRequest(ctx, req)
if err != nil {
log.Printf("Error occurred in NewAuthorizeRequest: %+v", err)
utils.Error("Error occurred in NewAuthorizeRequest:", err)
oauth2.WriteAuthorizeError(ctx, rw, ar, err)
return
}
@ -41,27 +39,6 @@ func authEndpoint(rw http.ResponseWriter, req *http.Request) {
// Now that the user is authorized, we set up a session:
mySessionData := newSession(nickname, req)
// When using the HMACSHA strategy you must use something that implements the HMACSessionContainer.
// It brings you the power of overriding the default values.
//
// mySessionData.HMACSession = &strategy.HMACSession{
// AccessTokenExpiry: time.Now().Add(time.Day),
// AuthorizeCodeExpiry: time.Now().Add(time.Day),
// }
//
// If you're using the JWT strategy, there's currently no distinction between access token and authorize code claims.
// Therefore, you both access token and authorize code will have the same "exp" claim. If this is something you
// need let us know on github.
//
// mySessionData.JWTClaims.ExpiresAt = time.Now().Add(time.Day)
// It's also wise to check the requested scopes, e.g.:
// if ar.GetRequestedScopes().Has("admin") {
// http.Error(rw, "you're not allowed to do that", http.StatusForbidden)
// return
// }
// Now we need to get a response. This is the place where the AuthorizeEndpointHandlers kick in and start processing the request.
// NewAuthorizeResponse is capable of running multiple response type handlers which in turn enables this library
// to support open id connect.
@ -69,10 +46,8 @@ func authEndpoint(rw http.ResponseWriter, req *http.Request) {
// Catch any errors, e.g.:
// * unknown client
// * invalid redirect
// * ...
if err != nil {
log.Printf("Error occurred in NewAuthorizeResponse: %+v", err)
utils.Error("Error occurred in NewAuthorizeResponse:", err)
oauth2.WriteAuthorizeError(ctx, rw, ar, err)
return
}

View file

@ -9,200 +9,35 @@ import (
"github.com/azukaar/cosmos-server/src/utils"
)
// OpenID Connect Discovery Metadata
//
// Includes links to several endpoints (for example `/oauth2/token`) and exposes information on supported signature algorithms
// among others.
//
// swagger:model oidcConfiguration
type oidcConfiguration struct {
// OpenID Connect Issuer URL
//
// An URL using the https scheme with no query or fragment component that the OP asserts as its IssuerURL Identifier.
// If IssuerURL discovery is supported , this value MUST be identical to the issuer value returned
// by WebFinger. This also MUST be identical to the iss Claim value in ID Tokens issued from this IssuerURL.
//
// required: true
// example: https://playground.ory.sh/ory-hydra/public/
Issuer string `json:"issuer"`
// OAuth 2.0 Authorization Endpoint URL
//
// required: true
// example: https://playground.ory.sh/ory-hydra/public/oauth2/auth
AuthURL string `json:"authorization_endpoint"`
// OpenID Connect Dynamic Client Registration Endpoint URL
//
// example: https://playground.ory.sh/ory-hydra/admin/client
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
// OAuth 2.0 Token Endpoint URL
//
// required: true
// example: https://playground.ory.sh/ory-hydra/public/oauth2/token
TokenURL string `json:"token_endpoint"`
// OpenID Connect Well-Known JSON Web Keys URL
//
// URL of the OP's JSON Web Key Set [JWK] document. This contains the signing key(s) the RP uses to validate
// signatures from the OP. The JWK Set MAY also contain the Server's encryption key(s), which are used by RPs
// to encrypt requests to the Server. When both signing and encryption keys are made available, a use (Key Use)
// parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage.
// Although some algorithms allow the same key to be used for both signatures and encryption, doing so is
// NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide X.509 representations of
// keys provided. When used, the bare key values MUST still be present and MUST match those in the certificate.
//
// required: true
// example: https://{slug}.projects.oryapis.com/.well-known/jwks.json
JWKsURI string `json:"jwks_uri"`
// OpenID Connect Supported Subject Types
//
// JSON array containing a list of the Subject Identifier types that this OP supports. Valid types include
// pairwise and public.
//
// required: true
// example:
// - public
// - pairwise
SubjectTypes []string `json:"subject_types_supported"`
// OAuth 2.0 Supported Response Types
//
// JSON array containing a list of the OAuth 2.0 response_type values that this OP supports. Dynamic OpenID
// Providers MUST support the code, id_token, and the token id_token Response Type values.
//
// required: true
ResponseTypes []string `json:"response_types_supported"`
// OpenID Connect Supported Claims
//
// JSON array containing a list of the Claim Names of the Claims that the OpenID Provider MAY be able to supply
// values for. Note that for privacy or other reasons, this might not be an exhaustive list.
ClaimsSupported []string `json:"claims_supported"`
// OAuth 2.0 Supported Grant Types
//
// JSON array containing a list of the OAuth 2.0 Grant Type values that this OP supports.
GrantTypesSupported []string `json:"grant_types_supported"`
// OAuth 2.0 Supported Response Modes
//
// JSON array containing a list of the OAuth 2.0 response_mode values that this OP supports.
ResponseModesSupported []string `json:"response_modes_supported"`
// OpenID Connect Userinfo URL
//
// URL of the OP's UserInfo Endpoint.
UserinfoEndpoint string `json:"userinfo_endpoint"`
// OAuth 2.0 Supported Scope Values
//
// JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports. The server MUST
// support the openid scope value. Servers MAY choose not to advertise some supported scope values even when this parameter is used
ScopesSupported []string `json:"scopes_supported"`
// OAuth 2.0 Supported Client Authentication Methods
//
// JSON array containing a list of Client Authentication methods supported by this Token Endpoint. The options are
// client_secret_post, client_secret_basic, client_secret_jwt, and private_key_jwt, as described in Section 9 of OpenID Connect Core 1.0
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
// OpenID Connect Supported Userinfo Signing Algorithm
//
// JSON array containing a list of the JWS [JWS] signing algorithms (alg values) [JWA] supported by the UserInfo Endpoint to encode the Claims in a JWT [JWT].
UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported"`
// OpenID Connect Supported ID Token Signing Algorithms
//
// JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for the ID Token
// to encode the Claims in a JWT.
//
// required: true
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
// OpenID Connect Default ID Token Signing Algorithms
//
// Algorithm used to sign OpenID Connect ID Tokens.
//
// required: true
IDTokenSignedResponseAlg []string `json:"id_token_signed_response_alg"`
// OpenID Connect User Userinfo Signing Algorithm
//
// Algorithm used to sign OpenID Connect Userinfo Responses.
//
// required: true
UserinfoSignedResponseAlg []string `json:"userinfo_signed_response_alg"`
// OpenID Connect Request Parameter Supported
//
// Boolean value specifying whether the OP supports use of the request parameter, with true indicating support.
RequestParameterSupported bool `json:"request_parameter_supported"`
// OpenID Connect Request URI Parameter Supported
//
// Boolean value specifying whether the OP supports use of the request_uri parameter, with true indicating support.
RequestURIParameterSupported bool `json:"request_uri_parameter_supported"`
// OpenID Connect Requires Request URI Registration
//
// Boolean value specifying whether the OP requires any request_uri values used to be pre-registered
// using the request_uris registration parameter.
RequireRequestURIRegistration bool `json:"require_request_uri_registration"`
// OpenID Connect Claims Parameter Parameter Supported
//
// Boolean value specifying whether the OP supports use of the claims parameter, with true indicating support.
ClaimsParameterSupported bool `json:"claims_parameter_supported"`
// OAuth 2.0 Token Revocation URL
//
// URL of the authorization server's OAuth 2.0 revocation endpoint.
RevocationEndpoint string `json:"revocation_endpoint"`
// OpenID Connect Back-Channel Logout Supported
//
// Boolean value specifying whether the OP supports back-channel logout, with true indicating support.
BackChannelLogoutSupported bool `json:"backchannel_logout_supported"`
// OpenID Connect Back-Channel Logout Session Required
//
// Boolean value specifying whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP
// session with the OP. If supported, the sid Claim is also included in ID Tokens issued by the OP
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported"`
// OpenID Connect Front-Channel Logout Supported
//
// Boolean value specifying whether the OP supports HTTP-based logout, with true indicating support.
FrontChannelLogoutSupported bool `json:"frontchannel_logout_supported"`
// OpenID Connect Front-Channel Logout Session Required
//
// Boolean value specifying whether the OP can pass iss (issuer) and sid (session ID) query parameters to identify
// the RP session with the OP when the frontchannel_logout_uri is used. If supported, the sid Claim is also
// included in ID Tokens issued by the OP.
FrontChannelLogoutSessionSupported bool `json:"frontchannel_logout_session_supported"`
// OpenID Connect End-Session Endpoint
//
// URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.
EndSessionEndpoint string `json:"end_session_endpoint"`
// OpenID Connect Supported Request Object Signing Algorithms
//
// JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for Request Objects,
// which are described in Section 6.1 of OpenID Connect Core 1.0 [OpenID.Core]. These algorithms are used both when
// the Request Object is passed by value (using the request parameter) and when it is passed by reference
// (using the request_uri parameter).
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"`
// OAuth 2.0 PKCE Supported Code Challenge Methods
//
// JSON array containing a list of Proof Key for Code Exchange (PKCE) [RFC7636] code challenge methods supported
// by this authorization server.
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}

View file

@ -1,8 +1,8 @@
package authorizationserver
import (
"log"
"net/http"
"github.com/azukaar/cosmos-server/src/utils"
)
func introspectionEndpoint(rw http.ResponseWriter, req *http.Request) {
@ -10,7 +10,7 @@ func introspectionEndpoint(rw http.ResponseWriter, req *http.Request) {
mySessionData := newSession("", req)
ir, err := oauth2.NewIntrospectionRequest(ctx, req, mySessionData)
if err != nil {
log.Printf("Error occurred in NewIntrospectionRequest: %+v", err)
utils.Error("Error occurred in NewIntrospectionRequest", err)
oauth2.WriteIntrospectionError(ctx, rw, err)
return
}

View file

@ -5,7 +5,7 @@ import (
"net/http"
// "fmt"
// "github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/utils"
)
func tokenEndpoint(rw http.ResponseWriter, req *http.Request) {
@ -46,6 +46,8 @@ func tokenEndpoint(rw http.ResponseWriter, req *http.Request) {
return
}
utils.Log("Access token granted to client: " + accessRequest.GetClient().GetID())
// All done, send the response.
oauth2.WriteAccessResponse(ctx, rw, accessRequest, response)

View file

@ -38,6 +38,8 @@ func userInfosEndpoint(rw http.ResponseWriter, req *http.Request) {
errorDescription := "Only access tokens are allowed in the authorization header."
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer error="invalid_token",error_description="%s"`, errorDescription))
// h.r.Writer().WriteErrorCode(w, r, http.StatusUnauthorized, errors.New(errorDescription))
utils.Error("UserInfosGet: Only access tokens are allowed in the authorization header", err)
utils.HTTPError(rw, "Only access tokens are allowed in the authorization header", http.StatusInternalServerError, "UD001")
return
}
@ -52,7 +54,7 @@ func userInfosEndpoint(rw http.ResponseWriter, req *http.Request) {
return
}
utils.Debug("UserGet: Get user " + nickname)
utils.Debug("UserInfosGet: Get user " + nickname)
user := utils.User{}
@ -61,7 +63,7 @@ func userInfosEndpoint(rw http.ResponseWriter, req *http.Request) {
}).Decode(&user)
if err != nil {
utils.Error("UserGet: Error while getting user", err)
utils.Error("UserInfosGet: Error while getting user", err)
utils.HTTPError(rw, "User Get Error", http.StatusInternalServerError, "UD001")
return
}

View file

@ -4,6 +4,7 @@ import (
"net/http"
"encoding/json"
"github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/authorizationserver"
)
func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
@ -38,6 +39,8 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
utils.SaveConfigTofile(request)
utils.NeedsRestart = true
authorizationserver.Init()
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})

View file

@ -7,6 +7,7 @@ import (
"github.com/azukaar/cosmos-server/src/docker"
"github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/authorizationserver"
)
func main() {
@ -36,5 +37,7 @@ func main() {
utils.Log("Docker API version: " + version.APIVersion)
}
authorizationserver.Init()
StartServer()
}

View file

@ -85,6 +85,7 @@ type Config struct {
ServerCountry string
RequireMFA bool
AutoUpdate bool
OpenIDClients []OpenIDClient
}
type HTTPConfig struct {
@ -159,4 +160,10 @@ type EmailConfig struct {
Password string
From string
UseTLS bool
}
type OpenIDClient struct {
ID string `json:"id"`
Secret string `json:"secret"`
Redirect string `json:"redirect"`
}