Примеры кода
Готовый вход через UNIT ID для популярных стеков. Везде — Authorization Code с PKCE; секрет берётся из переменной окружения.
Python · Flask + Authlib
pip install flask authlib requests
import os
from authlib.integrations.flask_client import OAuth
from flask import Flask, redirect, session, url_for
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET"]
oauth = OAuth(app)
oauth.register(
"unit",
client_id=os.environ["UNIT_CLIENT_ID"],
client_secret=os.environ["UNIT_CLIENT_SECRET"],
server_metadata_url="https://auth.labunit.ru/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"},
)
@app.get("/login")
def login():
return oauth.unit.authorize_redirect(url_for("callback", _external=True))
@app.get("/auth/callback")
def callback():
token = oauth.unit.authorize_access_token() # проверит state, PKCE и подпись id_token
person = token["userinfo"]
session["user"] = {"id": person["sub"], "name": person.get("name"), "tags": person.get("unit_tags", [])}
return redirect("/")
@app.get("/logout")
def logout():
session.clear()
return redirect("https://auth.labunit.ru/logout?next=" + url_for("index", _external=True))
Python · без библиотек
import base64, hashlib, os, secrets, requests
from urllib.parse import urlencode
ISSUER = "https://auth.labunit.ru"
CLIENT_ID, CLIENT_SECRET = os.environ["UNIT_CLIENT_ID"], os.environ["UNIT_CLIENT_SECRET"]
REDIRECT = "https://myapp.ru/auth/callback"
def start(session):
verifier, state = secrets.token_urlsafe(48), secrets.token_urlsafe(16)
session["unit"] = {"verifier": verifier, "state": state}
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
return ISSUER + "/oauth/authorize?" + urlencode({
"client_id": CLIENT_ID, "response_type": "code", "redirect_uri": REDIRECT,
"scope": "openid profile email", "state": state,
"code_challenge": challenge, "code_challenge_method": "S256"})
def finish(session, args):
saved = session.pop("unit", None) or {}
if not saved or args.get("state") != saved.get("state") or args.get("error"):
raise PermissionError("вход отменён или подделан")
tokens = requests.post(ISSUER + "/oauth/token", auth=(CLIENT_ID, CLIENT_SECRET), timeout=15, data={
"grant_type": "authorization_code", "code": args["code"],
"redirect_uri": REDIRECT, "code_verifier": saved["verifier"]}).json()
me = requests.get(ISSUER + "/oauth/userinfo", timeout=15,
headers={"Authorization": "Bearer " + tokens["access_token"]}).json()
return me # {"sub": …, "name": …, "email": …}
Node.js · Express + openid-client
npm i express express-session openid-client@5
import express from "express";
import session from "express-session";
import { Issuer, generators } from "openid-client";
const unit = await Issuer.discover("https://auth.labunit.ru");
const client = new unit.Client({
client_id: process.env.UNIT_CLIENT_ID,
client_secret: process.env.UNIT_CLIENT_SECRET,
redirect_uris: ["https://myapp.ru/auth/callback"],
response_types: ["code"],
});
const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));
app.get("/login", (req, res) => {
const verifier = generators.codeVerifier(), state = generators.state();
req.session.unit = { verifier, state };
res.redirect(client.authorizationUrl({
scope: "openid profile email", state,
code_challenge: generators.codeChallenge(verifier), code_challenge_method: "S256",
}));
});
app.get("/auth/callback", async (req, res) => {
const { verifier, state } = req.session.unit || {};
const tokens = await client.callback("https://myapp.ru/auth/callback", client.callbackParams(req),
{ code_verifier: verifier, state });
const claims = tokens.claims(); // id_token уже проверен
req.session.user = { id: claims.sub, name: claims.name };
res.redirect("/");
});
app.listen(3000);
Браузерное приложение (публичное)
Приложение без сервера регистрируется как «Публичное». Обмен кода — прямо из браузера, без секрета:
async function pkce() {
const verifier = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(48))))
.replace(/[+/=]/g, (c) => ({ "+": "-", "/": "_", "=": "" })[c]);
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/[+/=]/g, (c) => ({ "+": "-", "/": "_", "=": "" })[c]);
return { verifier, challenge };
}
// после возврата на redirect_uri
const body = new URLSearchParams({ grant_type: "authorization_code", code, client_id: CLIENT_ID,
redirect_uri: REDIRECT, code_verifier: sessionStorage.getItem("verifier") });
const tokens = await fetch("https://auth.labunit.ru/oauth/token", { method: "POST", body }).then((r) => r.json());
Если запрос из браузера не проходитОбмен кода и запросы к API из браузера на чужом домене упираются в CORS. Надёжнее держать небольшой серверный обработчик, который меняет код на токены, — или напишите администратору UNIT, чтобы для вашего домена открыли доступ.
Telegram-бот
Бот не может открыть страницу входа сам. Дайте человеку ссылку на ваш сервер (/login?tg=ИД_ЧАТА), проведите обычный вход, а в обработчике возврата свяжите sub из UNIT ID с чатом. Дальше бот узнаёт человека по чату.