feat: sso login flow
This commit is contained in:
parent
6f90d381f3
commit
a282427f24
|
|
@ -29,7 +29,7 @@ APP_NAME=${1:-$(generate_default_app_name)}
|
|||
export VITE_APP_ENVIRONMENT="dev-$APP_NAME"
|
||||
|
||||
if [[ "$APP_NAME" == "myvbv-stage" ]]; then
|
||||
export VITE_OAUTH_API_BASE_URL="https://vbvtst.b2clogin.com/vbvtst.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/"
|
||||
export VITE_OAUTH_API_BASE_URL="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/"
|
||||
export VITE_APP_ENVIRONMENT="stage-caprover"
|
||||
elif [[ "$APP_NAME" == prod* ]]; then
|
||||
export VITE_OAUTH_API_BASE_URL="https://edumgr.b2clogin.com/edumgr.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/"
|
||||
|
|
|
|||
|
|
@ -274,7 +274,9 @@ onMounted(() => {
|
|||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
<div v-else><a class="" href="/login">Login</a></div>
|
||||
<div v-else>
|
||||
<a class="" :href="`/sso/login?lang=${userStore.language}`">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const userStore = useUserStore();
|
|||
{{ $t("login.ssoText") }}
|
||||
</p>
|
||||
<p class="btn-primary mt-8">
|
||||
<a :href="`/sso/login/?lang=${userStore.language}`">
|
||||
<a :href="`/sso/login?lang=${userStore.language}`">
|
||||
{{ $t("login.ssoLogin") }}
|
||||
</a>
|
||||
</p>
|
||||
|
|
@ -100,6 +100,15 @@ const userStore = useUserStore();
|
|||
{{ $t("login.demoLogin") }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="mt-8">
|
||||
<a href="/sso/signup">Test SSO Signup (TODO)</a>
|
||||
</p>
|
||||
<p class="mt-8">
|
||||
<a href="/sso/signup?course=vv">Test SSO Signup VV (TODO)</a>
|
||||
</p>
|
||||
<p class="mt-8">
|
||||
<a href="/sso/signup?course=uk">Test SSO Signup UK (TODO)</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-medium">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
|
||||
const props = defineProps({
|
||||
courseType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const user = useUserStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -7,10 +16,15 @@ import WizardPage from "@/components/onboarding/WizardPage.vue";
|
|||
<template #content>
|
||||
<h2 class="my-10">Konto erstellen</h2>
|
||||
<p class="mb-4">Damit du myVBV nutzen kannst, brauchst du ein Konto.</p>
|
||||
<a href="#" class="btn-primary">Konto erstellen</a>
|
||||
<a
|
||||
:href="`/sso/signup?course=${props.courseType}&lang=${user.language}`"
|
||||
class="btn-primary"
|
||||
>
|
||||
Konto erstellen
|
||||
</a>
|
||||
|
||||
<p class="mb-4 mt-12">Hast du schon ein Konto?</p>
|
||||
<a href="/sso/login/" class="btn-secondary">Anmelden</a>
|
||||
<a :href="`/sso/login?lang=${user.language}`" class="btn-secondary">Anmelden</a>
|
||||
</template>
|
||||
</WizardPage>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ const router = createRouter({
|
|||
path: "account/create",
|
||||
component: () => import("@/pages/onboarding/AccountSetup.vue"),
|
||||
name: "accountCreate",
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "account/confirm",
|
||||
|
|
|
|||
|
|
@ -558,7 +558,14 @@ else:
|
|||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
|
||||
ALLOWED_HOSTS = env.list(
|
||||
"IT_DJANGO_ALLOWED_HOSTS", default=["localhost", "0.0.0.0", "127.0.0.1"]
|
||||
"IT_DJANGO_ALLOWED_HOSTS",
|
||||
# FIXME @livioso: Remove ngrok -> Datatrans Testing (hope I don't commit this)
|
||||
default=[
|
||||
"localhost",
|
||||
"0.0.0.0",
|
||||
"127.0.0.1",
|
||||
"e124-2a02-21b4-9679-d800-9c5-c205-e72c-82f2.ngrok-free.app",
|
||||
],
|
||||
)
|
||||
|
||||
# CACHES
|
||||
|
|
@ -584,38 +591,45 @@ if "django_redis.cache.RedisCache" in env("IT_DJANGO_CACHE_BACKEND", default="")
|
|||
},
|
||||
}
|
||||
|
||||
# OAuth/OpenId Connect
|
||||
IT_OAUTH_TENANT_ID = env.str("IT_OAUTH_TENANT_ID", default=None)
|
||||
# OAuth (SSO) settings
|
||||
OAUTH_SIGNUP_TENANT_ID = env("OAUTH_SIGNUP_TENANT_ID", default=None)
|
||||
OAUTH_SIGNUP_PARAMS = (
|
||||
{"tenant_id": OAUTH_SIGNUP_TENANT_ID} if OAUTH_SIGNUP_TENANT_ID else {}
|
||||
)
|
||||
|
||||
if IT_OAUTH_TENANT_ID:
|
||||
IT_OAUTH_AUTHORIZE_PARAMS = {"tenant_id": IT_OAUTH_TENANT_ID}
|
||||
else:
|
||||
IT_OAUTH_AUTHORIZE_PARAMS = {}
|
||||
|
||||
OAUTH = {
|
||||
"client_name": env("IT_OAUTH_CLIENT_NAME", default="lernetz"),
|
||||
"client_id": env("IT_OAUTH_CLIENT_ID", default="iterativ"),
|
||||
"client_secret": env("IT_OAUTH_CLIENT_SECRET", default=""),
|
||||
"authorize_params": IT_OAUTH_AUTHORIZE_PARAMS,
|
||||
"access_token_params": IT_OAUTH_AUTHORIZE_PARAMS,
|
||||
"api_base_url": env(
|
||||
"IT_OAUTH_API_BASE_URL",
|
||||
default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/",
|
||||
),
|
||||
"local_redirect_uri": env(
|
||||
"IT_OAUTH_LOCAL_REDIRECT_URI", default="http://localhost:8000/sso/callback/"
|
||||
),
|
||||
"server_metadata_url": env(
|
||||
"IT_OAUTH_SERVER_METADATA_URL",
|
||||
default="https://sso.test.b.lernetz.host/auth/realms/vbv/.well-known/openid-configuration",
|
||||
),
|
||||
"client_kwargs": {
|
||||
"scope": env("IT_OAUTH_SCOPE", default="openid email"),
|
||||
"token_endpoint_auth_method": "client_secret_post",
|
||||
"token_placement": "body",
|
||||
AUTHLIB_OAUTH_CLIENTS = {
|
||||
"signup": {
|
||||
# azure
|
||||
"client_id": env("OAUTH_SIGNUP_CLIENT_ID", ""),
|
||||
"client_secret": env("OAUTH_SIGNUP_CLIENT_SECRET", ""),
|
||||
"server_metadata_url": env("OAUTH_SIGNUP_SERVER_METADATA_URL", ""),
|
||||
"access_token_params": OAUTH_SIGNUP_PARAMS,
|
||||
"authorize_params": OAUTH_SIGNUP_PARAMS,
|
||||
"client_kwargs": {
|
||||
"scope": "openid",
|
||||
"token_endpoint_auth_method": "client_secret_post",
|
||||
"token_placement": "body",
|
||||
},
|
||||
},
|
||||
"signin": {
|
||||
# keycloak
|
||||
"client_id": env("OAUTH_SIGNIN_CLIENT_ID", ""),
|
||||
"client_secret": env("OAUTH_SIGNIN_CLIENT_SECRET", ""),
|
||||
"server_metadata_url": env("OAUTH_SIGNIN_SERVER_METADATA_URL", ""),
|
||||
"client_kwargs": {
|
||||
"scope": "openid email profile",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
OAUTH_SIGNUP_REDIRECT_URI = env(
|
||||
"OAUTH_SIGNUP_REDIRECT_URI", default="http://localhost:8000/sso/login"
|
||||
)
|
||||
|
||||
OAUTH_SIGNIN_REDIRECT_URI = env(
|
||||
"OAUTH_SIGNIN_REDIRECT_URI", default="http://localhost:8000/sso/callback"
|
||||
)
|
||||
|
||||
GRAPHENE = {
|
||||
"SCHEMA": "vbv_lernwelt.core.schema.schema",
|
||||
"SCHEMA_OUTPUT": "../client/src/gql/schema.graphql",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.consts import (
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
|
||||
|
||||
def has_course_session_user_vv(user: User) -> bool:
|
||||
vv_course_ids = [
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||
]
|
||||
|
||||
return CourseSession.objects.filter(
|
||||
course__id__in=vv_course_ids, coursesessionuser__user=user
|
||||
).exists()
|
||||
|
|
@ -216,7 +216,7 @@ def create_or_update_user(
|
|||
sso_id: str = None,
|
||||
contract_number: str = "",
|
||||
date_of_birth: str = "",
|
||||
):
|
||||
) -> User:
|
||||
logger.debug(
|
||||
"create_or_update_user",
|
||||
email=email,
|
||||
|
|
|
|||
|
|
@ -1,188 +1,74 @@
|
|||
# Datatrans - Proof of Concept
|
||||
# Setup
|
||||
|
||||
## Setup manual steps
|
||||
## Shop Product
|
||||
|
||||
- `HMAC_KEY`: https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp
|
||||
- `BASIC_AUTH`: `echo -n "{merchantid}:{password}" | base64` (`merchantid`: `11xxxxxx`): https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4
|
||||
- In Django Shop App, create a new product (Products model).
|
||||
- `SKU` must be `VV`, Price 30000 (300_00 -> 300.00 CHF), name & description can be anything.
|
||||
- Done for staging but not yet for production!
|
||||
|
||||
## Links
|
||||
## Datatrans
|
||||
|
||||
- https://admin.sandbox.datatrans.com
|
||||
- https://api-reference.datatrans.ch/#section/Idempotency
|
||||
- https://docs.datatrans.ch/docs/redirect-lightbox#section-initializing-transactions
|
||||
- Set `DATATRANS_BASIC_AUTH_KEY`:
|
||||
- https://admin.sandbox.datatrans.com/MenuDispatch.jsp?main=1&sub=4
|
||||
- `echo -n "{merchantid}:{password}" | base64`
|
||||
|
||||
## Code
|
||||
- Set `DATATRANS_HMAC_KEY`:
|
||||
- https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp
|
||||
|
||||
Simple example of the payment flow with Datatrans:
|
||||
- Ensure that the webhook is set up correctly by Datatrans:
|
||||
- Be default transitions from `initialized` to `failed` do not trigger the webhook.
|
||||
- Edgecase: When user starts a datatrans payment and then closes the browser, the payment will be in `initialized`
|
||||
state forever. -> That's why we need the webhook for `initialized` -> `failed` transitions.
|
||||
- This can and needs to be enabled by datatrans (according to Mario from datatrans).
|
||||
- Livio 21.11.23: Mario promised to enable it,
|
||||
- Livio 27.11.23. Not yet enabled for the sandbox. -> Followed up!
|
||||
- Livio: TODO still not enabled. Follow up again!
|
||||
|
||||
```python
|
||||
### Production / "going live"
|
||||
|
||||
from flask import Flask, request, render_template_string, jsonify, abort
|
||||
import uuid
|
||||
import hmac
|
||||
import hashlib
|
||||
import requests
|
||||
import os
|
||||
For Production: We use the proper production datatrans endpoint!
|
||||
|
||||
app = Flask(__name__)
|
||||
1. Coordinate with datatrans to get production account.
|
||||
2. Set `DATATRANS_BASIC_AUTH_KEY` and `DATATRANS_HMAC_KEY` to the production values (see above).
|
||||
3. Ensure that the webhook is set up correctly by Datatrans (see above).
|
||||
|
||||
if "HMAC_KEY" not in os.environ:
|
||||
exit("Please set the HMAC_KEY environment variable.")
|
||||
## OAUTH
|
||||
|
||||
if "BASIC_AUTH" not in os.environ:
|
||||
exit("Please set the BASIC_AUTH environment variable.")
|
||||
Make sure that the following env vars are set:
|
||||
|
||||
# https://admin.sandbox.datatrans.com/MerchSecurAdmin.jsp
|
||||
HMAC_KEY = os.environ["HMAC_KEY"]
|
||||
BASIC_AUTH = os.environ["BASIC_AUTH"]
|
||||
API_ENDPOINT = "https://api.sandbox.datatrans.com/v1/transactions"
|
||||
### Azure B2C
|
||||
|
||||
LIGHTBOX_PAGE = """
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://pay.sandbox.datatrans.com/upp/payment/js/datatrans-2.0.0.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="payButton">Pay Now</button>
|
||||
<script>
|
||||
var payButton = document.getElementById('payButton');
|
||||
payButton.onclick = function() {
|
||||
Datatrans.startPayment({
|
||||
transactionId: "{{ transaction_id }}",
|
||||
opened: function() { console.log('payment-form opened'); },
|
||||
loaded: function() { console.log('payment-form loaded'); },
|
||||
closed: function() { console.log('payment-page closed'); },
|
||||
error: function(errorData) { console.log('error', errorData); }
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
- Set `OAUTH_SIGNUP_CLIENT_ID`
|
||||
- Set `OAUTH_SIGNUP_CLIENT_SECRET`
|
||||
- Set `OAUTH_SIGNUP_SERVER_METADATA_URL` (.well-known/openid-configuration)
|
||||
- Set `OAUTH_SIGNUP_TENANT_ID`
|
||||
|
||||
SUCCESS_PAGE = "<html><body><h1>Payment Success</h1></body></html>"
|
||||
ERROR_PAGE = "<html><body><h1>Payment Error</h1></body></html>"
|
||||
CANCEL_PAGE = "<html><body><h1>Payment Cancelled</h1></body></html>"
|
||||
### Keycloak
|
||||
|
||||
# TODO: There is now way to test this locally, so we need to use ngrok (?)
|
||||
BASE_URL = "https://89d3-2a02-21b4-9679-d800-ac5f-489-e9f6-694e.ngrok-free.app"
|
||||
- Set `OAUTH_SIGNIN_CLIENT_ID`
|
||||
- Set `OAUTH_SIGNIN_CLIENT_SECRET`
|
||||
- Set `OAUTH_SIGNIN_SERVER_METADATA_URL` (.well-known/openid-configuration)
|
||||
|
||||
### Redirect URIs
|
||||
|
||||
@app.route("/success", methods=["GET"])
|
||||
def success():
|
||||
return render_template_string(SUCCESS_PAGE)
|
||||
- Set `OAUTH_SIGNUP_REDIRECT_URI` (`.../sso/login` e.g. `https://myvbv-stage.iterativ.ch/sso/login`)
|
||||
- Set `OAUTH_SIGNIN_REDIRECT_URI` (`.../sso/callback` e.g. `https://myvbv-stage.iterativ.ch/sso/callback`)
|
||||
|
||||
### Frontend:
|
||||
|
||||
@app.route("/error", methods=["GET"])
|
||||
def error():
|
||||
return render_template_string(ERROR_PAGE)
|
||||
- Update `VITE_OAUTH_API_BASE_URL` in `caprover_deploy.sh` for production.
|
||||
- NEEDS to be updated! Should be the SSO Prod one from Lernnetz -> Lookup from Metadata URL
|
||||
|
||||
### Cleanup
|
||||
|
||||
@app.route("/cancel", methods=["GET"])
|
||||
def cancel():
|
||||
return render_template_string(CANCEL_PAGE)
|
||||
After everything runs fine, we should be able to remove the following env vars:
|
||||
|
||||
1. `IT_OAUTH_TENANT_ID`
|
||||
2. `IT_OAUTH_CLIENT_NAME`
|
||||
3. `IT_OAUTH_CLIENT_ID`
|
||||
4. `IT_OAUTH_CLIENT_SECRET`
|
||||
5. `IT_OAUTH_API_BASE_URL`
|
||||
6. `IT_OAUTH_LOCAL_REDIRECT_URI`
|
||||
7. `IT_OAUTH_SERVER_METADATA_URL`
|
||||
8. `IT_OAUTH_SCOPE`
|
||||
|
||||
@app.route("/init_transaction", methods=["GET"])
|
||||
def init_transaction():
|
||||
# TODO
|
||||
# for debugging, it might be handy to know the
|
||||
# user who initiated the transaction
|
||||
refno = uuid.uuid4().hex
|
||||
|
||||
# TODO
|
||||
# The language of user
|
||||
language = "en"
|
||||
|
||||
# Transaction payload
|
||||
payload = {
|
||||
"currency": "CHF",
|
||||
"refno": refno,
|
||||
"amount": 10_00, # 10 CHF
|
||||
"autoSettle": True,
|
||||
"language": language,
|
||||
"redirect": {
|
||||
"successUrl": f"{BASE_URL}/success",
|
||||
"errorUrl": f"{BASE_URL}/error",
|
||||
"cancelUrl": f"{BASE_URL}/cancel",
|
||||
},
|
||||
"webhook": {
|
||||
"url": f"{BASE_URL}/webhook",
|
||||
},
|
||||
}
|
||||
|
||||
# Headers
|
||||
headers = {
|
||||
"Authorization": f"Basic {BASIC_AUTH}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# 1. USING LIGHTBOX
|
||||
response = requests.post(API_ENDPOINT, json=payload, headers=headers)
|
||||
|
||||
if response.ok:
|
||||
transaction_id = response.json().get("transactionId")
|
||||
return render_template_string(LIGHTBOX_PAGE, transaction_id=transaction_id)
|
||||
else:
|
||||
return (
|
||||
jsonify(
|
||||
{"error": "Failed to initiate transaction", "details": response.text}
|
||||
),
|
||||
response.status_code,
|
||||
)
|
||||
|
||||
# 2. USING REDIRECT
|
||||
# # Send POST request to Datatrans API
|
||||
# response = requests.post(url, json=payload, headers=headers)
|
||||
|
||||
# if response.ok:
|
||||
# transaction_id = response.json().get('transactionId')
|
||||
# payment_url = f'https://pay.sandbox.datatrans.com/v1/start/{transaction_id}'
|
||||
# return redirect(payment_url)
|
||||
# else:
|
||||
# # Return error message
|
||||
# return jsonify({"error": "Failed to initiate transaction", "details": response.text}), response.status_code
|
||||
|
||||
|
||||
@app.route("/webhook", methods=["POST"])
|
||||
def webhook():
|
||||
"""
|
||||
Checks the Datatrans-Signature header of the incoming request and validates the signature:
|
||||
https://api-reference.datatrans.ch/#section/Webhook/Webhook-signing
|
||||
"""
|
||||
|
||||
# TODO Check the state here too!
|
||||
|
||||
hmac_key = HMAC_KEY
|
||||
|
||||
def calculate_signature(key: str, timestamp: str, payload: str) -> str:
|
||||
key_bytes = bytes.fromhex(key)
|
||||
signing_data = f"{timestamp}{payload}".encode("utf-8")
|
||||
hmac_obj = hmac.new(key_bytes, signing_data, hashlib.sha256)
|
||||
return hmac_obj.hexdigest()
|
||||
|
||||
# Header format:
|
||||
# Datatrans-Signature: t={{timestamp}},s0={{signature}}
|
||||
datatrans_signature = request.headers.get("Datatrans-Signature", "")
|
||||
|
||||
try:
|
||||
parts = datatrans_signature.split(",")
|
||||
timestamp = parts[0].split("=")[1]
|
||||
received_signature = parts[1].split("=")[1]
|
||||
|
||||
calculated_signature = calculate_signature(
|
||||
hmac_key, timestamp, request.data.decode("utf-8")
|
||||
)
|
||||
|
||||
if calculated_signature == received_signature:
|
||||
return "Signature validated.", 200
|
||||
else:
|
||||
abort(400, "Invalid signature.")
|
||||
except (IndexError, ValueError):
|
||||
abort(400, "Invalid Datatrans-Signature header.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host="0.0.0.0", port=5500)
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
from authlib.integrations.django_client import OAuth
|
||||
from django.conf import settings
|
||||
|
||||
# # https://docs.authlib.org/en/latest/client/frameworks.html#frameworks-clients
|
||||
oauth = OAuth()
|
||||
oauth.register(
|
||||
name=settings.OAUTH["client_name"],
|
||||
client_id=settings.OAUTH["client_id"],
|
||||
client_secret=settings.OAUTH["client_secret"],
|
||||
request_token_url=None,
|
||||
request_token_params=None,
|
||||
authorize_params=settings.OAUTH["authorize_params"],
|
||||
client_kwargs=settings.OAUTH["client_kwargs"],
|
||||
server_metadata_url=settings.OAUTH["server_metadata_url"],
|
||||
)
|
||||
oauth.register(name="signup")
|
||||
oauth.register(name="signin")
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ from . import views
|
|||
|
||||
app_name = "sso"
|
||||
urlpatterns = [
|
||||
path(r"login/", django_view_authentication_exempt(views.login), name="login"),
|
||||
path(r"login/", django_view_authentication_exempt(views.signin), name="login"),
|
||||
path(r"signup/", django_view_authentication_exempt(views.signup), name="signup"),
|
||||
path(
|
||||
r"callback/",
|
||||
django_view_authentication_exempt(views.authorize),
|
||||
django_view_authentication_exempt(views.authorize_signin),
|
||||
name="authorize",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from django.contrib.auth import login as dj_login
|
|||
from django.shortcuts import redirect
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.course_session.utils import has_course_session_user_vv
|
||||
from vbv_lernwelt.importer.services import create_or_update_user
|
||||
from vbv_lernwelt.sso.client import oauth
|
||||
from vbv_lernwelt.sso.jwt import decode_jwt
|
||||
|
|
@ -14,44 +16,56 @@ logger = structlog.get_logger(__name__)
|
|||
OAUTH_FAIL_REDIRECT = "login-error"
|
||||
|
||||
|
||||
def login(request):
|
||||
oauth_client = oauth.create_client(settings.OAUTH["client_name"])
|
||||
redirect_uri = settings.OAUTH["local_redirect_uri"]
|
||||
language = request.GET.get("lang", "de")
|
||||
return oauth_client.authorize_redirect(request, redirect_uri, lang=language)
|
||||
def signup(request):
|
||||
course = request.GET.get("course")
|
||||
redirect_uri = settings.OAUTH_SIGNUP_REDIRECT_URI
|
||||
logger.debug(f"SSO Signup (course={course})", sso_signup_redirect_uri=redirect_uri)
|
||||
return oauth.signup.authorize_redirect(request, redirect_uri, state=course)
|
||||
|
||||
|
||||
def authorize(request):
|
||||
def signin(request):
|
||||
# course query OR state when coming from signup (oauth)
|
||||
course = request.GET.get("course", request.GET.get("state"))
|
||||
redirect_uri = settings.OAUTH_SIGNIN_REDIRECT_URI
|
||||
logger.info(f"SSO Login (course={course})", sso_login_redirect_uri=redirect_uri)
|
||||
return oauth.signin.authorize_redirect(
|
||||
request, redirect_uri, state=course, lang=request.GET.get("lang", "de")
|
||||
)
|
||||
|
||||
|
||||
def authorize_signin(request):
|
||||
try:
|
||||
logger.debug(request, label="sso")
|
||||
token = getattr(oauth, settings.OAUTH["client_name"]).authorize_access_token(
|
||||
request
|
||||
)
|
||||
decoded_token = decode_jwt(token["id_token"])
|
||||
# logger.debug(label="sso", decoded_token=decoded_token)
|
||||
jwt_token = oauth.signin.authorize_access_token(request)
|
||||
except OAuthError as e:
|
||||
logger.error(e, exc_info=True, label="sso")
|
||||
if not settings.DEBUG:
|
||||
capture_exception(e)
|
||||
return redirect(f"/{OAUTH_FAIL_REDIRECT}?state=someerror") # to be defined
|
||||
return redirect(f"/{OAUTH_FAIL_REDIRECT}?state=oautherror")
|
||||
|
||||
id_token = decode_jwt(jwt_token["id_token"])
|
||||
course = request.GET.get("state")
|
||||
|
||||
logger.debug(
|
||||
f"SSO Authorize (course={course})",
|
||||
sso_authorize_id_token=id_token,
|
||||
)
|
||||
|
||||
user_data = _user_data_from_token_data(decoded_token)
|
||||
user = create_or_update_user(
|
||||
email=user_data.get("email").lower(),
|
||||
sso_id=user_data.get("sso_id"),
|
||||
first_name=user_data.get("first_name", ""),
|
||||
last_name=user_data.get("last_name", ""),
|
||||
email=id_token.get("email", ""),
|
||||
sso_id=id_token.get("oid"),
|
||||
first_name=id_token.get("given_name", ""),
|
||||
last_name=id_token.get("family_name", ""),
|
||||
)
|
||||
|
||||
dj_login(request, user)
|
||||
return redirect(f"/")
|
||||
|
||||
|
||||
def _user_data_from_token_data(token: dict) -> dict:
|
||||
first_email = token.get("emails", [""])[0]
|
||||
return {
|
||||
"first_name": token.get("given_name", ""),
|
||||
"last_name": token.get("family_name", ""),
|
||||
"email": first_email,
|
||||
"sso_id": token.get("oid"),
|
||||
}
|
||||
# figure out where to redirect to (onboarding or home)
|
||||
if course == "vv" and not has_course_session_user_vv(user):
|
||||
return redirect("/onboarding/vv/account/create")
|
||||
elif (
|
||||
course == "uk"
|
||||
and not CourseSession.objects.filter(coursesessionuser__user=user).exists()
|
||||
):
|
||||
return redirect("/onboarding/uk/account/create")
|
||||
else:
|
||||
return redirect("/")
|
||||
|
|
|
|||
Loading…
Reference in New Issue