mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2025-07-13 16:46:58 +00:00
Spent 4 hours working on OIDC verification
This commit is contained in:
parent
66bd1da571
commit
380b9a73ab
@ -1,8 +1,10 @@
|
|||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
|
from authlib.integrations.flask_client import OAuth
|
||||||
|
import authlib
|
||||||
|
|
||||||
from functools import wraps
|
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
|
import os
|
||||||
|
|
||||||
from modules.WireguardConfiguration import WireguardConfiguration
|
from modules.WireguardConfiguration import WireguardConfiguration
|
||||||
@ -28,6 +30,10 @@ def login_required(f):
|
|||||||
|
|
||||||
def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration], dashboardConfig: DashboardConfig):
|
def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration], dashboardConfig: DashboardConfig):
|
||||||
from modules.DashboardClients import DashboardClients
|
from modules.DashboardClients import DashboardClients
|
||||||
|
from modules.DashboardOIDC import DashboardOIDC
|
||||||
|
|
||||||
|
|
||||||
|
OIDC = DashboardOIDC()
|
||||||
DashboardClients = DashboardClients(wireguardConfigurations)
|
DashboardClients = DashboardClients(wireguardConfigurations)
|
||||||
client = Blueprint('client', __name__, template_folder=os.path.abspath("./static/client/dist"))
|
client = Blueprint('client', __name__, template_folder=os.path.abspath("./static/client/dist"))
|
||||||
prefix = f'{dashboardConfig.GetConfig("Server", "app_prefix")[1]}/client'
|
prefix = f'{dashboardConfig.GetConfig("Server", "app_prefix")[1]}/client'
|
||||||
@ -37,7 +43,6 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
def clientBeforeRequest():
|
def clientBeforeRequest():
|
||||||
if request.method.lower() == 'options':
|
if request.method.lower() == 'options':
|
||||||
return ResponseObject(True)
|
return ResponseObject(True)
|
||||||
|
|
||||||
|
|
||||||
@client.post(f'{prefix}/api/signup')
|
@client.post(f'{prefix}/api/signup')
|
||||||
def ClientAPI_SignUp():
|
def ClientAPI_SignUp():
|
||||||
@ -45,6 +50,13 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
status, msg = DashboardClients.SignUp(**data)
|
status, msg = DashboardClients.SignUp(**data)
|
||||||
return ResponseObject(status, msg)
|
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')
|
@client.post(f'{prefix}/api/signin')
|
||||||
def ClientAPI_SignIn():
|
def ClientAPI_SignIn():
|
||||||
data = request.json
|
data = request.json
|
||||||
|
@ -14,8 +14,9 @@ from .Utilities import (
|
|||||||
from .DashboardAPIKey import DashboardAPIKey
|
from .DashboardAPIKey import DashboardAPIKey
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardConfig:
|
class DashboardConfig:
|
||||||
DashboardVersion = 'v4.2.3'
|
DashboardVersion = 'v4.3'
|
||||||
ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.')
|
ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.')
|
||||||
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard.ini')
|
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard.ini')
|
||||||
|
|
||||||
|
84
src/modules/DashboardOIDC.py
Normal file
84
src/modules/DashboardOIDC.py
Normal 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()
|
||||||
|
)
|
@ -12,4 +12,5 @@ sqlalchemy
|
|||||||
sqlalchemy_utils
|
sqlalchemy_utils
|
||||||
psycopg2
|
psycopg2
|
||||||
mysqlclient
|
mysqlclient
|
||||||
tzlocal
|
tzlocal
|
||||||
|
python-jose
|
@ -5,7 +5,7 @@ import "animate.css"
|
|||||||
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
|
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
|
||||||
import LocaleText from "@/components/text/localeText.vue";
|
import LocaleText from "@/components/text/localeText.vue";
|
||||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||||
import {GetLocale} from "../../utilities/locale.js";
|
import {GetLocale} from "@/utilities/locale.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "peer",
|
name: "peer",
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
src/static/client/dist/client.html
vendored
4
src/static/client/dist/client.html
vendored
@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" href="/static/client/dist/img/Logo-2-128x128.png">
|
<link rel="icon" href="/static/client/dist/img/Logo-2-128x128.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vite App</title>
|
<title>Vite App</title>
|
||||||
<script type="module" crossorigin src="/static/client/dist/assets/index-C7xxuubz.js"></script>
|
<script type="module" crossorigin src="/static/client/dist/assets/index-VLGPfnEQ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/static/client/dist/assets/index-DtDMqk5z.css">
|
<link rel="stylesheet" crossorigin href="/static/client/dist/assets/index-BmGX7v2g.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/Logo-2-128x128.png">
|
<link rel="icon" href="/img/Logo-2-128x128.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
22
src/static/client/package-lock.json
generated
22
src/static/client/package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"bootstrap": "^5.3.6",
|
"bootstrap": "^5.3.6",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"oidc-client-ts": "^3.2.1",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
@ -2524,6 +2525,15 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"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": {
|
"node_modules/kolorist": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
|
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
|
||||||
@ -2670,6 +2680,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/open": {
|
||||||
"version": "10.1.2",
|
"version": "10.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/open/-/open-10.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/open/-/open-10.1.2.tgz",
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"bootstrap": "^5.3.6",
|
"bootstrap": "^5.3.6",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"oidc-client-ts": "^3.2.1",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
@ -39,6 +39,16 @@ const route = useRoute()
|
|||||||
if (route.query.Email){
|
if (route.query.Email){
|
||||||
formData.Email = 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -47,6 +57,19 @@ if (route.query.Email){
|
|||||||
<h1 class="display-4">Welcome back</h1>
|
<h1 class="display-4">Welcome back</h1>
|
||||||
<p class="text-muted">Sign in to access your <strong>WGDashboard Client</strong> account</p>
|
<p class="text-muted">Sign in to access your <strong>WGDashboard Client</strong> account</p>
|
||||||
</div>
|
</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)">
|
<form class="mt-4 d-flex flex-column gap-3" @submit="e => signIn(e)">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
@ -7,12 +7,24 @@ import {createPinia} from "pinia";
|
|||||||
|
|
||||||
import 'bootstrap/dist/js/bootstrap.bundle.js'
|
import 'bootstrap/dist/js/bootstrap.bundle.js'
|
||||||
import {clientStore} from "@/stores/clientStore.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)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.mount("#app")
|
app.mount("#app")
|
6
src/static/client/src/utilities/oidcAuth.js
Normal file
6
src/static/client/src/utilities/oidcAuth.js
Normal 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')
|
||||||
|
|
||||||
|
}
|
@ -18,6 +18,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user