mirror of
https://github.com/rommapp/romm.git
synced 2026-03-03 02:27:00 +00:00
start work with fake openid add for testing
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,6 +37,7 @@ __pycache__
|
||||
|
||||
# database
|
||||
mariadb
|
||||
*.sqlite
|
||||
|
||||
# used to mock the library/config/mounts/etc while testing
|
||||
frontend/assets/romm
|
||||
|
||||
@@ -61,6 +61,13 @@ DISABLE_DOWNLOAD_ENDPOINT_AUTH = (
|
||||
os.environ.get("DISABLE_DOWNLOAD_ENDPOINT_AUTH", "false") == "true"
|
||||
)
|
||||
|
||||
# OAUTH
|
||||
OAUTH_ENABLED: Final = os.environ.get("OAUTH_ENABLED", "false") == "true"
|
||||
OAUTH_CLIENT_ID: Final = os.environ.get("OAUTH_CLIENT_ID", "")
|
||||
OAUTH_CLIENT_SECRET: Final = os.environ.get("OAUTH_CLIENT_SECRET", "")
|
||||
OAUTH_REDIRECT_URI: Final = os.environ.get("OAUTH_REDIRECT_URI", "")
|
||||
OAUTH_SERVER_METADATA_URL: Final = os.environ.get("OAUTH_SERVER_METADATA_URL", "")
|
||||
|
||||
# SCANS
|
||||
SCAN_TIMEOUT: Final = int(os.environ.get("SCAN_TIMEOUT", 60 * 60 * 4)) # 4 hours
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Security
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from config import (
|
||||
OAUTH_CLIENT_ID,
|
||||
OAUTH_CLIENT_SECRET,
|
||||
OAUTH_ENABLED,
|
||||
OAUTH_REDIRECT_URI,
|
||||
OAUTH_SERVER_METADATA_URL,
|
||||
)
|
||||
from fastapi import Depends, Security
|
||||
from fastapi.security.http import HTTPBasic
|
||||
from fastapi.security.oauth2 import OAuth2PasswordBearer
|
||||
from fastapi.security.oauth2 import OAuth2AuthorizationCodeBearer, OAuth2PasswordBearer
|
||||
from fastapi.types import DecoratedCallable
|
||||
from handler.auth.base_handler import (
|
||||
DEFAULT_SCOPES_MAP,
|
||||
@@ -10,6 +18,7 @@ from handler.auth.base_handler import (
|
||||
WRITE_SCOPES_MAP,
|
||||
)
|
||||
from starlette.authentication import requires
|
||||
from starlette.config import Config
|
||||
|
||||
oauth2_password_bearer = OAuth2PasswordBearer(
|
||||
tokenUrl="/token",
|
||||
@@ -21,6 +30,29 @@ oauth2_password_bearer = OAuth2PasswordBearer(
|
||||
},
|
||||
)
|
||||
|
||||
config = Config(
|
||||
environ={
|
||||
"OAUTH_ENABLED": OAUTH_ENABLED,
|
||||
"OAUTH_CLIENT_ID": OAUTH_CLIENT_ID,
|
||||
"OAUTH_CLIENT_SECRET": OAUTH_CLIENT_SECRET,
|
||||
"OAUTH_REDIRECT_URI": OAUTH_REDIRECT_URI,
|
||||
"OAUTH_SERVER_METADATA_URL": OAUTH_SERVER_METADATA_URL,
|
||||
}
|
||||
)
|
||||
oauth = OAuth(config=config)
|
||||
oauth.register(
|
||||
name="openid",
|
||||
client_id=config.get("OAUTH_CLIENT_ID"),
|
||||
client_secret=config.get("OAUTH_CLIENT_SECRET"),
|
||||
server_metadata_url=config.get("OAUTH_SERVER_METADATA_URL"),
|
||||
client_kwargs={"scope": "openid profile email"},
|
||||
)
|
||||
|
||||
oauth2_autorization_code_bearer = OAuth2AuthorizationCodeBearer(
|
||||
authorizationUrl="/auth/openid",
|
||||
tokenUrl="/token",
|
||||
)
|
||||
|
||||
|
||||
def protected_route(
|
||||
method: Any,
|
||||
@@ -38,6 +70,13 @@ def protected_route(
|
||||
scopes=scopes or [],
|
||||
),
|
||||
Security(dependency=HTTPBasic(auto_error=False)),
|
||||
(
|
||||
Security(
|
||||
dependency=oauth2_autorization_code_bearer, scopes=scopes or []
|
||||
)
|
||||
if OAUTH_ENABLED
|
||||
else Depends(lambda: None)
|
||||
),
|
||||
],
|
||||
**kwargs,
|
||||
)(fn)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Annotated, Final
|
||||
|
||||
from config import OAUTH_ENABLED, OAUTH_REDIRECT_URI
|
||||
from decorators.auth import oauth
|
||||
from endpoints.forms.identity import OAuth2RequestForm
|
||||
from endpoints.responses import MessageResponse
|
||||
from endpoints.responses.oauth import TokenResponse
|
||||
@@ -134,7 +136,8 @@ async def token(form_data: Annotated[OAuth2RequestForm, Depends()]) -> TokenResp
|
||||
|
||||
@router.post("/login")
|
||||
def login(
|
||||
request: Request, credentials=Depends(HTTPBasic()) # noqa
|
||||
request: Request,
|
||||
credentials=Depends(HTTPBasic()), # noqa
|
||||
) -> MessageResponse:
|
||||
"""Session login endpoint
|
||||
|
||||
@@ -167,6 +170,53 @@ def login(
|
||||
return {"msg": "Successfully logged in"}
|
||||
|
||||
|
||||
@router.get("/login/openid")
|
||||
async def login_via_openid(request: Request):
|
||||
"""OAuth2 login endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
|
||||
Returns:
|
||||
RedirectResponse: Redirect to OAuth2 provider
|
||||
"""
|
||||
|
||||
if not OAUTH_ENABLED:
|
||||
raise DisabledException
|
||||
|
||||
return await oauth.openid.authorize_redirect(request, OAUTH_REDIRECT_URI)
|
||||
|
||||
|
||||
@router.get("/auth/openid")
|
||||
async def auth_openid(request: Request):
|
||||
"""OAuth2 callback endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
|
||||
Returns:
|
||||
RedirectResponse: Redirect to home page
|
||||
"""
|
||||
|
||||
token = await oauth.openid.authorize_access_token(request)
|
||||
user, claims = await oauth_handler.get_current_active_user_from_bearer_token(token)
|
||||
|
||||
if not user:
|
||||
raise AuthCredentialsException
|
||||
|
||||
if not user.enabled:
|
||||
raise DisabledException
|
||||
|
||||
request.session.update({"iss": "romm:auth", "sub": user.username})
|
||||
|
||||
# Update last login and active times
|
||||
db_user_handler.update_user(
|
||||
user.id, {"last_login": datetime.now(), "last_active": datetime.now()}
|
||||
)
|
||||
|
||||
return {"msg": "Successfully logged in"}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(request: Request) -> MessageResponse:
|
||||
"""Session logout endpoint
|
||||
|
||||
@@ -19,11 +19,16 @@ async function login(
|
||||
);
|
||||
}
|
||||
|
||||
async function loginOIDC(): Promise<{ data: MessageResponse }> {
|
||||
return api.get("/login/openid");
|
||||
}
|
||||
|
||||
async function logout(): Promise<{ data: MessageResponse }> {
|
||||
return api.post("/logout", {});
|
||||
return api.post("/logout");
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
loginOIDC,
|
||||
logout,
|
||||
};
|
||||
|
||||
@@ -43,6 +43,19 @@ async function login() {
|
||||
logging.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function loginOIDC() {
|
||||
logging.value = true;
|
||||
await identityApi
|
||||
.loginOIDC()
|
||||
.then(() => {
|
||||
const next = (router.currentRoute.value.query?.next || "/").toString();
|
||||
router.push(next);
|
||||
})
|
||||
.finally(() => {
|
||||
logging.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -103,6 +116,30 @@ async function login() {
|
||||
/>
|
||||
</template>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
:disabled="logging"
|
||||
:variant="'text'"
|
||||
class="bg-terciary"
|
||||
block
|
||||
:loading="logging"
|
||||
@click="loginOIDC()"
|
||||
>
|
||||
<span>Login with OIDC</span>
|
||||
<template #append>
|
||||
<v-icon class="text-romm-accent-1"
|
||||
>mdi-chevron-right-circle-outline</v-icon
|
||||
>
|
||||
</template>
|
||||
<template #loader>
|
||||
<v-progress-circular
|
||||
color="romm-accent-1"
|
||||
:width="2"
|
||||
:size="20"
|
||||
indeterminate
|
||||
/>
|
||||
</template>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
96
oidc-provider/index.js
Normal file
96
oidc-provider/index.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { config } from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import { Provider } from "oidc-provider";
|
||||
import express from "express";
|
||||
|
||||
import SQLiteAdapter from "./sqlite_adapter.js";
|
||||
|
||||
config({ path: resolve("../.env") });
|
||||
|
||||
const DEFAULT_SCOPES_MAP = {
|
||||
"me.read": "View your profile",
|
||||
"me.write": "Modify your profile",
|
||||
"roms.read": "View ROMs",
|
||||
"platforms.read": "View platforms",
|
||||
"assets.read": "View assets",
|
||||
"assets.write": "Modify assets",
|
||||
"firmware.read": "View firmware",
|
||||
"roms.user.read": "View user-rom properties",
|
||||
"roms.user.write": "Modify user-rom properties",
|
||||
"collections.read": "View collections",
|
||||
"collections.write": "Modify collections",
|
||||
};
|
||||
|
||||
const WRITE_SCOPES_MAP = {
|
||||
"roms.write": "Modify ROMs",
|
||||
"platforms.write": "Modify platforms",
|
||||
"firmware.write": "Modify firmware",
|
||||
};
|
||||
|
||||
const FULL_SCOPES_MAP = {
|
||||
"users.read": "View users",
|
||||
"users.write": "Modify users",
|
||||
"tasks.run": "Run tasks",
|
||||
};
|
||||
|
||||
const ALL_SCOPES = [
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
...Object.keys(DEFAULT_SCOPES_MAP),
|
||||
...Object.keys(WRITE_SCOPES_MAP),
|
||||
...Object.keys(FULL_SCOPES_MAP),
|
||||
];
|
||||
|
||||
const app = express();
|
||||
const clients = [
|
||||
{
|
||||
client_id: process.env.OAUTH_CLIENT_ID,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET,
|
||||
grant_types: ["authorization_code"],
|
||||
redirect_uris: [process.env.OAUTH_REDIRECT_URI],
|
||||
response_types: ["code"],
|
||||
scope: ALL_SCOPES.join(" "),
|
||||
},
|
||||
];
|
||||
|
||||
const oidc = new Provider("http://localhost:4000", {
|
||||
clients,
|
||||
features: {
|
||||
introspection: { enabled: true },
|
||||
revocation: { enabled: true },
|
||||
devInteractions: { enabled: true },
|
||||
},
|
||||
pkce: { methods: ["S256"], required: () => false },
|
||||
formats: {
|
||||
AccessToken: "jwt",
|
||||
},
|
||||
jwks: {
|
||||
keys: [
|
||||
{
|
||||
kty: "RSA",
|
||||
kid: "test-key",
|
||||
use: "sig",
|
||||
e: "AQAB",
|
||||
n: "test-key-n",
|
||||
d: "test-key-d",
|
||||
p: "test-key-p",
|
||||
q: "test-key-q",
|
||||
dp: "test-key-dp",
|
||||
dq: "test-key-dq",
|
||||
qi: "test-key-qi",
|
||||
},
|
||||
],
|
||||
},
|
||||
cookies: {
|
||||
keys: [process.env.SIGNING_COOKIE_A, process.env.SIGNING_COOKIE_B],
|
||||
},
|
||||
scopes: ALL_SCOPES,
|
||||
adapter: SQLiteAdapter,
|
||||
});
|
||||
|
||||
app.use("/oidc", oidc.callback());
|
||||
|
||||
app.listen(4000, () => {
|
||||
console.log("OIDC provider listening on http://localhost:4000");
|
||||
});
|
||||
2600
oidc-provider/package-lock.json
generated
Normal file
2600
oidc-provider/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
oidc-provider/package.json
Normal file
20
oidc-provider/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "oidc-provider",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.1.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"oidc-provider": "^8.5.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
}
|
||||
}
|
||||
58
oidc-provider/sqlite_adapter.js
Normal file
58
oidc-provider/sqlite_adapter.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { resolve } from "path";
|
||||
|
||||
const dbPath = resolve("./data/oidc_data.sqlite");
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Initialize the database schema
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS oidc (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
expiresAt INTEGER
|
||||
)
|
||||
`);
|
||||
|
||||
class SQLiteAdapter {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
async upsert(id, payload, expiresIn) {
|
||||
const expiresAt = expiresIn ? Date.now() + expiresIn * 1000 : null;
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO oidc (id, name, payload, expiresAt)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
payload=excluded.payload,
|
||||
expiresAt=excluded.expiresAt
|
||||
`);
|
||||
stmt.run(id, this.name, JSON.stringify(payload), expiresAt);
|
||||
}
|
||||
|
||||
async find(id) {
|
||||
const stmt = db.prepare(`SELECT * FROM oidc WHERE id = ? AND name = ?`);
|
||||
const record = stmt.get(id, this.name);
|
||||
if (!record) return undefined;
|
||||
if (record.expiresAt && record.expiresAt < Date.now()) {
|
||||
await this.destroy(id);
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(record.payload);
|
||||
}
|
||||
|
||||
async destroy(id) {
|
||||
const stmt = db.prepare(`DELETE FROM oidc WHERE id = ? AND name = ?`);
|
||||
stmt.run(id, this.name);
|
||||
}
|
||||
|
||||
async revokeByGrantId(grantId) {
|
||||
const stmt = db.prepare(
|
||||
`DELETE FROM oidc WHERE json_extract(payload, '$.grantId') = ? AND name = ?`,
|
||||
);
|
||||
stmt.run(grantId, this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export default SQLiteAdapter;
|
||||
17
poetry.lock
generated
17
poetry.lock
generated
@@ -90,6 +90,20 @@ files = [
|
||||
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.3.1"
|
||||
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377"},
|
||||
{file = "authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = "*"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "4.1.3"
|
||||
@@ -1765,7 +1779,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@@ -2695,4 +2708,4 @@ multidict = ">=4.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "f8d8c12320cb8e5b47b182c507b4c988185910e60f7fb162f9508685072f2214"
|
||||
content-hash = "16d7552ecd58bac81f8a3957bc251a30c68943ad99d5272d35168100865b0342"
|
||||
|
||||
@@ -41,6 +41,7 @@ yarl = "^1.9.4"
|
||||
joserfc = "^0.9.0"
|
||||
pillow = "^10.3.0"
|
||||
certifi = "2024.07.04"
|
||||
authlib = "^1.3.1"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
fakeredis = "^2.21.3"
|
||||
|
||||
Reference in New Issue
Block a user