mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2025-07-13 08:36:59 +00:00
OIDC should be good to go
This commit is contained in:
parent
bf74150f62
commit
aa66a5ffb2
@ -21,7 +21,7 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
|
|||||||
def login_required(f):
|
def login_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def func(*args, **kwargs):
|
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 ResponseObject(False, "Unauthorized access.", data=None, status_code=401)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return func
|
return func
|
||||||
@ -60,8 +60,8 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
return ResponseObject(status, oidcData)
|
return ResponseObject(status, oidcData)
|
||||||
|
|
||||||
session['Email'] = oidcData.get('email')
|
session['Email'] = oidcData.get('email')
|
||||||
session['role'] = 'client'
|
session['Role'] = 'client'
|
||||||
session['totpVerified'] = True
|
session['TotpVerified'] = True
|
||||||
|
|
||||||
return ResponseObject()
|
return ResponseObject()
|
||||||
|
|
||||||
@ -71,15 +71,15 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
status, msg = DashboardClients.SignIn(**data)
|
status, msg = DashboardClients.SignIn(**data)
|
||||||
if status:
|
if status:
|
||||||
session['Email'] = data.get('Email')
|
session['Email'] = data.get('Email')
|
||||||
session['role'] = 'client'
|
session['Role'] = 'client'
|
||||||
session['totpVerified'] = False
|
session['TotpVerified'] = False
|
||||||
return ResponseObject(status, msg)
|
return ResponseObject(status, msg)
|
||||||
|
|
||||||
@client.get(f'{prefix}/api/signout')
|
@client.get(f'{prefix}/api/signout')
|
||||||
def ClientAPI_SignOut():
|
def ClientAPI_SignOut():
|
||||||
session['Email'] = None
|
if session.get("SignInMethod") == "OIDC":
|
||||||
session['role'] = None
|
DashboardClients.SignOut_OIDC()
|
||||||
session['totpVerified'] = None
|
session.clear()
|
||||||
return ResponseObject(True)
|
return ResponseObject(True)
|
||||||
|
|
||||||
@client.get(f'{prefix}/api/signin/totp')
|
@client.get(f'{prefix}/api/signin/totp')
|
||||||
@ -102,7 +102,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
if status:
|
if status:
|
||||||
if session.get('Email') is None:
|
if session.get('Email') is None:
|
||||||
return ResponseObject(False, "Sign in status is invalid", status_code=401)
|
return ResponseObject(False, "Sign in status is invalid", status_code=401)
|
||||||
session['totpVerified'] = True
|
session['TotpVerified'] = True
|
||||||
return ResponseObject(True, data={
|
return ResponseObject(True, data={
|
||||||
"Email": session.get('Email'),
|
"Email": session.get('Email'),
|
||||||
"Profile": DashboardClients.GetClientProfile(session.get("ClientID"))
|
"Profile": DashboardClients.GetClientProfile(session.get("ClientID"))
|
||||||
|
@ -4,6 +4,7 @@ import uuid
|
|||||||
import bcrypt
|
import bcrypt
|
||||||
import pyotp
|
import pyotp
|
||||||
import sqlalchemy as db
|
import sqlalchemy as db
|
||||||
|
import requests
|
||||||
|
|
||||||
from .ConnectionString import ConnectionString
|
from .ConnectionString import ConnectionString
|
||||||
from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment
|
from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment
|
||||||
@ -133,17 +134,32 @@ class DashboardClients:
|
|||||||
return True, newClientUUID
|
return True, newClientUUID
|
||||||
return False, "User already signed up"
|
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):
|
def SignIn_OIDC(self, **kwargs):
|
||||||
status, data = self.OIDC.VerifyToken(**kwargs)
|
status, data = self.OIDC.VerifyToken(**kwargs)
|
||||||
if not status:
|
if not status:
|
||||||
return False, "Sign in failed"
|
return False, "Sign in failed. Reason: " + data
|
||||||
existingClient = self.SignIn_OIDC_UserExistence(data)
|
existingClient = self.SignIn_OIDC_UserExistence(data)
|
||||||
if not existingClient:
|
if not existingClient:
|
||||||
status, newClientUUID = self.SignUp_OIDC(data)
|
status, newClientUUID = self.SignUp_OIDC(data)
|
||||||
session['ClientID'] = newClientUUID
|
session['ClientID'] = newClientUUID
|
||||||
else:
|
else:
|
||||||
session['ClientID'] = existingClient.get("ClientID")
|
session['ClientID'] = existingClient.get("ClientID")
|
||||||
|
session['SignInMethod'] = 'OIDC'
|
||||||
|
session['SignInPayload'] = {
|
||||||
|
"Provider": kwargs.get('provider'),
|
||||||
|
"Payload": data
|
||||||
|
}
|
||||||
return True, data
|
return True, data
|
||||||
|
|
||||||
def SignIn(self, Email, Password) -> tuple[bool, str]:
|
def SignIn(self, Email, Password) -> tuple[bool, str]:
|
||||||
@ -153,6 +169,7 @@ class DashboardClients:
|
|||||||
if existingClient:
|
if existingClient:
|
||||||
checkPwd = self.SignIn_ValidatePassword(Email, Password)
|
checkPwd = self.SignIn_ValidatePassword(Email, Password)
|
||||||
if checkPwd:
|
if checkPwd:
|
||||||
|
session['SignInMethod'] = 'local'
|
||||||
session['Email'] = Email
|
session['Email'] = Email
|
||||||
session['ClientID'] = existingClient.get("ClientID")
|
session['ClientID'] = existingClient.get("ClientID")
|
||||||
return True, self.DashboardClientsTOTP.GenerateToken(existingClient.get("ClientID"))
|
return True, self.DashboardClientsTOTP.GenerateToken(existingClient.get("ClientID"))
|
||||||
|
@ -10,6 +10,7 @@ class DashboardOIDC:
|
|||||||
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard-oidc-providers.json')
|
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard-oidc-providers.json')
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.providers: dict[str, dict] = {}
|
self.providers: dict[str, dict] = {}
|
||||||
|
self.provider_secret: dict[str, str] = {}
|
||||||
self.__default = {
|
self.__default = {
|
||||||
'Provider': {
|
'Provider': {
|
||||||
'client_id': '',
|
'client_id': '',
|
||||||
@ -26,52 +27,36 @@ class DashboardOIDC:
|
|||||||
self.ReadFile()
|
self.ReadFile()
|
||||||
|
|
||||||
def GetProviders(self):
|
def GetProviders(self):
|
||||||
providers = {}
|
return 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
|
|
||||||
|
|
||||||
def VerifyToken(self, provider, code, redirect_uri):
|
def VerifyToken(self, provider, code, redirect_uri):
|
||||||
try:
|
try:
|
||||||
if not all([provider, code, redirect_uri]):
|
if not all([provider, code, redirect_uri]):
|
||||||
return False, ""
|
return False, "Please provide all parameters"
|
||||||
|
|
||||||
if provider not in self.providers.keys():
|
if provider not in self.providers.keys():
|
||||||
return False, "Provider does not exist"
|
return False, "Provider does not exist"
|
||||||
|
|
||||||
provider = self.providers.get(provider)
|
secrete = self.provider_secret.get(provider)
|
||||||
oidc_config = requests.get(
|
oidc_config_status, oidc_config = self.GetProviderConfiguration(provider)
|
||||||
f"{provider.get('issuer').strip('/')}/.well-known/openid-configuration",
|
provider_info = self.providers.get(provider)
|
||||||
verify=certifi.where()
|
|
||||||
|
|
||||||
).json()
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"code": code,
|
"code": code,
|
||||||
"redirect_uri": redirect_uri,
|
"redirect_uri": redirect_uri,
|
||||||
"client_id": provider.get('client_id'),
|
"client_id": provider_info.get('client_id'),
|
||||||
"client_secret": provider.get('client_secret')
|
"client_secret": secrete
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tokens = requests.post(oidc_config.get('token_endpoint'), data=data).json()
|
tokens = requests.post(oidc_config.get('token_endpoint'), data=data).json()
|
||||||
if not all([tokens.get('access_token'), tokens.get('id_token')]):
|
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)
|
return False, tokens.get('error_description', None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(str(e))
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
access_token = tokens.get('access_token')
|
access_token = tokens.get('access_token')
|
||||||
@ -85,30 +70,57 @@ class DashboardOIDC:
|
|||||||
|
|
||||||
key = next(k for k in jwks["keys"] if k["kid"] == kid)
|
key = next(k for k in jwks["keys"] if k["kid"] == kid)
|
||||||
|
|
||||||
print(key)
|
|
||||||
|
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
id_token,
|
id_token,
|
||||||
key,
|
key,
|
||||||
algorithms=[key["alg"]],
|
algorithms=[key["alg"]],
|
||||||
audience=provider.get('client_id'),
|
audience=provider_info.get('client_id'),
|
||||||
issuer=issuer,
|
issuer=issuer,
|
||||||
access_token=access_token
|
access_token=access_token
|
||||||
)
|
)
|
||||||
|
print(payload)
|
||||||
return True, payload
|
return True, payload
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e), provider, code, redirect_uri)
|
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e), provider, code, redirect_uri)
|
||||||
return False, str(e)
|
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):
|
def ReadFile(self):
|
||||||
decoder = json.JSONDecoder()
|
decoder = json.JSONDecoder()
|
||||||
try:
|
try:
|
||||||
self.providers = decoder.decode(
|
providers = decoder.decode(
|
||||||
open(DashboardOIDC.ConfigurationFilePath, 'r').read()
|
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:
|
except Exception as e:
|
||||||
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e))
|
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e))
|
||||||
return False
|
return False
|
@ -21,13 +21,22 @@ const initApp = () => {
|
|||||||
app.mount("#app")
|
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){
|
if (state && code){
|
||||||
axiosPost("/api/signin/oidc", {
|
axiosPost("/api/signin/oidc", {
|
||||||
provider: state,
|
provider: state,
|
||||||
code: code,
|
code: code,
|
||||||
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
|
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
|
||||||
}).then(data => {
|
}).then(data => {
|
||||||
window.location.search = ''
|
let url = new URL(window.location.href);
|
||||||
|
url.search = '';
|
||||||
|
history.replaceState({}, document.title, url.toString());
|
||||||
|
|
||||||
initApp()
|
initApp()
|
||||||
if (!data.status){
|
if (!data.status){
|
||||||
const store = clientStore()
|
const store = clientStore()
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<script setup async>
|
<script setup async>
|
||||||
import {computed, onMounted, ref} from "vue";
|
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 {clientStore} from "@/stores/clientStore.js";
|
||||||
import Configuration from "@/components/Configuration/configuration.vue";
|
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 store = clientStore()
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
@ -23,7 +24,20 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
clearInterval(refreshInterval.value)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -37,10 +51,14 @@ onBeforeRouteLeave(() => {
|
|||||||
<i class="bi bi-gear-fill me-sm-2"></i>
|
<i class="bi bi-gear-fill me-sm-2"></i>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</RouterLink>
|
</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>
|
<i class="bi bi-box-arrow-left me-sm-2"></i>
|
||||||
<span>Sign Out</span>
|
<span>
|
||||||
</RouterLink>
|
{{ signingOut ? 'Signing out...':'Sign Out'}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user