Spent 4 hours working on OIDC verification

This commit is contained in:
Donald Zou 2025-06-28 18:13:26 +08:00
parent 66bd1da571
commit 380b9a73ab
15 changed files with 193 additions and 28 deletions

View File

@ -1,8 +1,10 @@
from tzlocal import get_localzone
from authlib.integrations.flask_client import OAuth
import authlib
from functools import wraps
from flask import Blueprint, render_template, abort, request, Flask, current_app, session
from flask import Blueprint, render_template, abort, request, Flask, current_app, session, redirect, url_for
import os
from modules.WireguardConfiguration import WireguardConfiguration
@ -28,6 +30,10 @@ def login_required(f):
def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration], dashboardConfig: DashboardConfig):
from modules.DashboardClients import DashboardClients
from modules.DashboardOIDC import DashboardOIDC
OIDC = DashboardOIDC()
DashboardClients = DashboardClients(wireguardConfigurations)
client = Blueprint('client', __name__, template_folder=os.path.abspath("./static/client/dist"))
prefix = f'{dashboardConfig.GetConfig("Server", "app_prefix")[1]}/client'
@ -38,13 +44,19 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
if request.method.lower() == 'options':
return ResponseObject(True)
@client.post(f'{prefix}/api/signup')
def ClientAPI_SignUp():
data = request.json
status, msg = DashboardClients.SignUp(**data)
return ResponseObject(status, msg)
@client.post(f'{prefix}/api/signin/oidc/')
def ClientAPI_SignIn_OIDC_Google():
data = request.json
print(OIDC.VerifyToken(**data))
return ResponseObject()
@client.post(f'{prefix}/api/signin')
def ClientAPI_SignIn():
data = request.json

View File

@ -14,8 +14,9 @@ from .Utilities import (
from .DashboardAPIKey import DashboardAPIKey
class DashboardConfig:
DashboardVersion = 'v4.2.3'
DashboardVersion = 'v4.3'
ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.')
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard.ini')

View File

@ -0,0 +1,84 @@
import os
import json
import requests
from jose import jwt
class DashboardOIDC:
ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.')
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard-oidc-providers.json')
def __init__(self):
self.providers: dict[str, dict] = None
self.__default = {
'Provider': {
'client_id': '',
'client_secret': '',
'issuer': '',
},
}
if not os.path.exists(DashboardOIDC.ConfigurationFilePath):
with open(DashboardOIDC.ConfigurationFilePath, "w+") as f:
encoder = json.JSONEncoder(indent=4)
f.write(encoder.encode(self.__default))
self.ReadFile()
def VerifyToken(self, provider, code, redirect_uri):
if not all([provider, code, redirect_uri]):
return False, ""
if provider not in self.providers.keys():
return False, "Provider does not exist"
provider = self.providers.get(provider)
oidc_config = requests.get(f"{provider.get('issuer')}.well-known/openid-configuration").json()
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirect_uri,
"client_id": provider.get('client_id'),
"client_secret": provider.get('client_secret')
}
tokens = requests.post(oidc_config.get('token_endpoint'), data=data).json()
id_token = tokens.get('id_token')
jwks_uri = oidc_config.get("jwks_uri")
issuer = oidc_config.get("issuer")
jwks = requests.get(jwks_uri).json()
from jose.utils import base64url_decode
from jose.backends.cryptography_backend import CryptographyRSAKey
# Choose the right key based on `kid` in token header
headers = jwt.get_unverified_header(id_token)
kid = headers["kid"]
# Find the key with the correct `kid`
key = next(k for k in jwks["keys"] if k["kid"] == kid)
# Use the key to verify token
payload = jwt.decode(
id_token,
key,
algorithms=[key["alg"]],
audience=provider.get('client_id'),
issuer=issuer
)
print(payload) # This contains the user's claims
def ReadFile(self):
decoder = json.JSONDecoder()
self.providers = decoder.decode(
open(DashboardOIDC.ConfigurationFilePath, 'r').read()
)

View File

@ -13,3 +13,4 @@ sqlalchemy_utils
psycopg2
mysqlclient
tzlocal
python-jose

View File

@ -5,7 +5,7 @@ import "animate.css"
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
import LocaleText from "@/components/text/localeText.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {GetLocale} from "../../utilities/locale.js";
import {GetLocale} from "@/utilities/locale.js";
export default {
name: "peer",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<link rel="icon" href="/static/client/dist/img/Logo-2-128x128.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script type="module" crossorigin src="/static/client/dist/assets/index-C7xxuubz.js"></script>
<link rel="stylesheet" crossorigin href="/static/client/dist/assets/index-DtDMqk5z.css">
<script type="module" crossorigin src="/static/client/dist/assets/index-VLGPfnEQ.js"></script>
<link rel="stylesheet" crossorigin href="/static/client/dist/assets/index-BmGX7v2g.css">
</head>
<body>
<div id="app"></div>

View File

@ -4,7 +4,9 @@
<meta charset="UTF-8">
<link rel="icon" href="/img/Logo-2-128x128.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>WGDashboard Client</title>
<script src="https://apis.google.com/js/platform.js" async defer></script>
<meta name="google-signin-client_id" content="140556036774-66pjm1qkml0v27ru1utsoeftsv3b4hid.apps.googleusercontent.com">
</head>
<body>
<div id="app"></div>

View File

@ -12,6 +12,7 @@
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13",
"oidc-client-ts": "^3.2.1",
"pinia": "^3.0.2",
"qrcode": "^1.5.4",
"uuid": "^11.1.0",
@ -2524,6 +2525,15 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
@ -2670,6 +2680,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/oidc-client-ts": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/oidc-client-ts/-/oidc-client-ts-3.2.1.tgz",
"integrity": "sha512-hS5AJ5s/x4bXhHvNJT1v+GGvzHUwdRWqNQQbSrp10L1IRmzfRGKQ3VWN3dstJb+oF3WtAyKezwD2+dTEIyBiAA==",
"license": "Apache-2.0",
"dependencies": {
"jwt-decode": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/open": {
"version": "10.1.2",
"resolved": "https://registry.npmmirror.com/open/-/open-10.1.2.tgz",

View File

@ -13,6 +13,7 @@
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13",
"oidc-client-ts": "^3.2.1",
"pinia": "^3.0.2",
"qrcode": "^1.5.4",
"uuid": "^11.1.0",

View File

@ -39,6 +39,16 @@ const route = useRoute()
if (route.query.Email){
formData.Email = route.query.Email
}
const params = new URLSearchParams({
client_id: "ijDjDnBCDuA75srtsq7ksxwpZkLjxiRZVdmkWnRC",
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname,
response_type: 'code',
state: 'Authentik',
scope: 'openid email profile'
}).toString()
console.log(params)
</script>
<template>
@ -47,6 +57,19 @@ if (route.query.Email){
<h1 class="display-4">Welcome back</h1>
<p class="text-muted">Sign in to access your <strong>WGDashboard Client</strong> account</p>
</div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-body rounded-3"
:href="'http://178.128.231.4:9000/application/o/authorize/?' + params"
style="flex: 1 1 0px;" >
Google
</a>
<button class="btn btn-sm btn-outline-body rounded-3" style="flex: 1 1 0px;">
GitHub
</button>
<button class="btn btn-sm btn-outline-body rounded-3" style="flex: 1 1 0px;">
Facebook
</button>
</div>
<form class="mt-4 d-flex flex-column gap-3" @submit="e => signIn(e)">
<div class="form-floating">
<input type="text"

View File

@ -7,12 +7,24 @@ import {createPinia} from "pinia";
import 'bootstrap/dist/js/bootstrap.bundle.js'
import {clientStore} from "@/stores/clientStore.js";
import {OIDCAuth} from "@/utilities/oidcAuth.js";
import {axiosPost} from "@/utilities/request.js";
const params = new URLSearchParams(window.location.search)
const state = params.get('state')
const code = params.get('code')
if (state && code){
axiosPost("/api/signin/oidc/", {
provider: state,
code: code,
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
}).then(data => {
console.log(data)
})
}
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount("#app")

View File

@ -0,0 +1,6 @@
export const OIDCAuth = async () => {
const params = new URLSearchParams(window.location.search)
const state = params.get('state')
const code = params.get('code')
}

View File

@ -18,6 +18,7 @@ export default defineConfig({
}
}
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))