OIDC should be good to go

This commit is contained in:
Donald Zou 2025-07-03 19:20:01 +08:00
parent bf74150f62
commit aa66a5ffb2
5 changed files with 109 additions and 53 deletions

View File

@ -21,7 +21,7 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
def login_required(f):
@wraps(f)
def func(*args, **kwargs):
if session.get("Email") is None or session.get("totpVerified") is None or not session.get("totpVerified") or session.get("role") != "client":
if session.get("Email") is None or session.get("TotpVerified") is None or not session.get("TotpVerified") or session.get("Role") != "client":
return ResponseObject(False, "Unauthorized access.", data=None, status_code=401)
return f(*args, **kwargs)
return func
@ -60,8 +60,8 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
return ResponseObject(status, oidcData)
session['Email'] = oidcData.get('email')
session['role'] = 'client'
session['totpVerified'] = True
session['Role'] = 'client'
session['TotpVerified'] = True
return ResponseObject()
@ -71,15 +71,15 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
status, msg = DashboardClients.SignIn(**data)
if status:
session['Email'] = data.get('Email')
session['role'] = 'client'
session['totpVerified'] = False
session['Role'] = 'client'
session['TotpVerified'] = False
return ResponseObject(status, msg)
@client.get(f'{prefix}/api/signout')
def ClientAPI_SignOut():
session['Email'] = None
session['role'] = None
session['totpVerified'] = None
if session.get("SignInMethod") == "OIDC":
DashboardClients.SignOut_OIDC()
session.clear()
return ResponseObject(True)
@client.get(f'{prefix}/api/signin/totp')
@ -102,7 +102,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
if status:
if session.get('Email') is None:
return ResponseObject(False, "Sign in status is invalid", status_code=401)
session['totpVerified'] = True
session['TotpVerified'] = True
return ResponseObject(True, data={
"Email": session.get('Email'),
"Profile": DashboardClients.GetClientProfile(session.get("ClientID"))

View File

@ -4,6 +4,7 @@ import uuid
import bcrypt
import pyotp
import sqlalchemy as db
import requests
from .ConnectionString import ConnectionString
from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment
@ -133,19 +134,34 @@ class DashboardClients:
return True, newClientUUID
return False, "User already signed up"
def SignOut_OIDC(self):
sessionPayload = session.get('OIDCPayload')
status, oidc_config = self.OIDC.GetProviderConfiguration(session.get('SignInPayload').get("Provider"))
signOut = requests.get(
oidc_config.get("end_session_endpoint"),
params={
'id_token_hint': session.get('SignInPayload').get("Payload").get('sid')
}
)
return True
def SignIn_OIDC(self, **kwargs):
status, data = self.OIDC.VerifyToken(**kwargs)
if not status:
return False, "Sign in failed"
return False, "Sign in failed. Reason: " + data
existingClient = self.SignIn_OIDC_UserExistence(data)
if not existingClient:
status, newClientUUID = self.SignUp_OIDC(data)
session['ClientID'] = newClientUUID
else:
session['ClientID'] = existingClient.get("ClientID")
session['SignInMethod'] = 'OIDC'
session['SignInPayload'] = {
"Provider": kwargs.get('provider'),
"Payload": data
}
return True, data
def SignIn(self, Email, Password) -> tuple[bool, str]:
if not all([Email, Password]):
return False, "Please fill in all fields"
@ -153,6 +169,7 @@ class DashboardClients:
if existingClient:
checkPwd = self.SignIn_ValidatePassword(Email, Password)
if checkPwd:
session['SignInMethod'] = 'local'
session['Email'] = Email
session['ClientID'] = existingClient.get("ClientID")
return True, self.DashboardClientsTOTP.GenerateToken(existingClient.get("ClientID"))

View File

@ -10,6 +10,7 @@ class DashboardOIDC:
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard-oidc-providers.json')
def __init__(self):
self.providers: dict[str, dict] = {}
self.provider_secret: dict[str, str] = {}
self.__default = {
'Provider': {
'client_id': '',
@ -26,52 +27,36 @@ class DashboardOIDC:
self.ReadFile()
def GetProviders(self):
providers = {}
for k in self.providers.keys():
if all([self.providers[k]['client_id'], self.providers[k]['client_secret'], self.providers[k]['issuer']]):
try:
print("Requesting " + f"{self.providers[k]['issuer'].strip('/')}/.well-known/openid-configuration")
oidc_config = requests.get(
f"{self.providers[k]['issuer'].strip('/')}/.well-known/openid-configuration",
verify=certifi.where()
).json()
providers[k] = {
'client_id': self.providers[k]['client_id'],
'issuer': self.providers[k]['issuer'].strip('/')
}
except Exception as e:
current_app.logger.error("Failed to request OIDC config for this provider: " + self.providers[k]['issuer'].strip('/'), exc_info=e)
return providers
return self.providers
def VerifyToken(self, provider, code, redirect_uri):
try:
if not all([provider, code, redirect_uri]):
return False, ""
return False, "Please provide all parameters"
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').strip('/')}/.well-known/openid-configuration",
verify=certifi.where()
).json()
secrete = self.provider_secret.get(provider)
oidc_config_status, oidc_config = self.GetProviderConfiguration(provider)
provider_info = self.providers.get(provider)
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirect_uri,
"client_id": provider.get('client_id'),
"client_secret": provider.get('client_secret')
"client_id": provider_info.get('client_id'),
"client_secret": secrete
}
try:
tokens = requests.post(oidc_config.get('token_endpoint'), data=data).json()
if not all([tokens.get('access_token'), tokens.get('id_token')]):
print(oidc_config.get('token_endpoint'), data)
return False, tokens.get('error_description', None)
except Exception as e:
print(str(e))
return False, str(e)
access_token = tokens.get('access_token')
@ -84,31 +69,58 @@ class DashboardOIDC:
kid = headers["kid"]
key = next(k for k in jwks["keys"] if k["kid"] == kid)
print(key)
payload = jwt.decode(
id_token,
key,
algorithms=[key["alg"]],
audience=provider.get('client_id'),
audience=provider_info.get('client_id'),
issuer=issuer,
access_token=access_token
)
print(payload)
return True, payload
except Exception as e:
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e), provider, code, redirect_uri)
return False, str(e)
def GetProviderConfiguration(self, provider_name):
if not all([provider_name]):
return False, None
provider = self.providers.get(provider_name)
try:
oidc_config = requests.get(
f"{provider.get('issuer').strip('/')}/.well-known/openid-configuration",
verify=certifi.where()
).json()
except Exception as e:
current_app.logger.error("Failed to get OpenID Configuration of " + provider.get('issuer'), exc_info=e)
return False, None
return True, oidc_config
def ReadFile(self):
decoder = json.JSONDecoder()
try:
self.providers = decoder.decode(
providers = decoder.decode(
open(DashboardOIDC.ConfigurationFilePath, 'r').read()
)
print(self.providers)
for k in providers.keys():
if all([providers[k]['client_id'], providers[k]['client_secret'], providers[k]['issuer']]):
try:
print("Requesting " + f"{providers[k]['issuer'].strip('/')}/.well-known/openid-configuration")
oidc_config = requests.get(
f"{providers[k]['issuer'].strip('/')}/.well-known/openid-configuration",
timeout=3,
verify=certifi.where()
).json()
self.providers[k] = {
'client_id': providers[k]['client_id'],
'issuer': providers[k]['issuer'].strip('/'),
'openid_configuration': oidc_config
}
self.provider_secret[k] = providers[k]['client_secret']
except Exception as e:
current_app.logger.error("Failed to request OIDC config for this provider: " + providers[k]['issuer'].strip('/'), exc_info=e)
except Exception as e:
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e))
return False

View File

@ -21,13 +21,22 @@ const initApp = () => {
app.mount("#app")
}
function removeSearchString() {
let url = new URL(window.location.href);
url.search = ''; // Remove all query parameters
history.replaceState({}, document.title, url.toString());
}
if (state && code){
axiosPost("/api/signin/oidc", {
provider: state,
code: code,
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
}).then(data => {
window.location.search = ''
let url = new URL(window.location.href);
url.search = '';
history.replaceState({}, document.title, url.toString());
initApp()
if (!data.status){
const store = clientStore()

View File

@ -1,9 +1,10 @@
<script setup async>
import {computed, onMounted, ref} from "vue";
import {axiosGet} from "@/utilities/request.js";
import {axiosGet, requestURl} from "@/utilities/request.js";
import {clientStore} from "@/stores/clientStore.js";
import Configuration from "@/components/Configuration/configuration.vue";
import {onBeforeRouteLeave} from "vue-router";
import {onBeforeRouteLeave, useRouter} from "vue-router";
import axios from "axios";
const store = clientStore()
const loading = ref(true)
@ -23,7 +24,20 @@ onMounted(async () => {
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value)
})
});
const router = useRouter()
const signingOut = ref(false)
const signOut = async () => {
clearInterval(refreshInterval.value)
signingOut.value = true;
await axios.get(requestURl('/api/signout')).then(() => {
router.push('/signin')
}).catch(() => {
router.push('/signin')
});
store.newNotification("Sign out successful", "success")
}
</script>
<template>
@ -37,10 +51,14 @@ onBeforeRouteLeave(() => {
<i class="bi bi-gear-fill me-sm-2"></i>
<span>Settings</span>
</RouterLink>
<RouterLink to="/signout" class="btn btn-outline-danger rounded-3 btn-sm" aria-current="page">
<a role="button" @click="signOut()" class="btn btn-outline-danger rounded-3 btn-sm"
:class="{disabled: signingOut}"
aria-current="page">
<i class="bi bi-box-arrow-left me-sm-2"></i>
<span>Sign Out</span>
</RouterLink>
<span>
{{ signingOut ? 'Signing out...':'Sign Out'}}
</span>
</a>
</div>
</div>