UNIT ID

Примеры кода

Готовый вход через 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 с чатом. Дальше бот узнаёт человека по чату.

UNIT · UNIT ID · вопросы по интеграции — администратору UNIT