React Router uygulamasına giriş ekleme

Bu hızlı başlangıç, framework modunda bir React Router 8 uygulamasına standart OpenID Connect istemcisi openid-client ile KeenAuth girişi ekler. Loader'lar ve action'lar sunucuda çalışır; bu nedenle uygulamanız OAuth istemcisidir. Tüm belirteçleri sunucu tarafındaki bir oturumda tutar, tarayıcıya yalnızca HTML ve imzalı bir oturum kimliği çerezi gönderilir. Giriş rotalarını ekleyecek, bir sayfayı koruyacak, korumalı bir API'yi kullanıcının erişim belirteciyle çağıracak, çıkış yapacak ve uygulamayı çevrimdışı test edeceksiniz.

Sunucusuz tek sayfalık bir uygulama için bkz. React tek sayfalık uygulamasına giriş ekleme.

Uygulamanın rotaları şunlardır:

Rota Ne yapar
GET / Herkese açık ana sayfa: bir giriş düğmesi veya kullanıcının adı ve bir çıkış düğmesi
GET /dashboard Oturum gerektirir. Kullanıcıyı ve API'nizin döndürdüğü iletileri gösterir
POST /login Girişi başlatır. Yalnızca bu uygulamanın bir sayfasından
GET /auth/callback Yanıtı denetler, kodu değiştirir ve oturumu başlatır
POST /logout Oturumu burada ve KeenAuth'ta sonlandırır. Yalnızca bu uygulamanın bir sayfasından

Başlamadan önce

1. Kurulum

npm install --save-exact openid-client@6.8.8

openid-client, OpenID Connect bağlanan taraf (relying party) işlevlerini uygular: keşif, PKCE ile yetkilendirme kodu akışı, RFC 9207 iss denetimi ve ID belirteci denetimleri. Örnek her paketi tam sürümüyle sabitler ve npm ci --ignore-scripts ile package-lock.json dosyasından kurar; böylece kurulum sırasında hiçbir paket betiği çalışmaz.

2. Yapılandırma

Uygulama yapılandırmasını yalnızca ortam değişkenlerinden okur. Bunları npm run dev ve npm start komutlarının node --env-file=.env ile yüklediği .env dosyasına koyun ve bu dosyayı asla commit etmeyin.

Değişken Zorunlu Anlamı
KEENAUTH_ISSUER evet Ortamınızın veren adresi, https
KEENAUTH_CLIENT_ID evet Web uygulamasının client_id değeri
KEENAUTH_CLIENT_SECRET evet Onun istemci gizli anahtarı
APP_BASE_URL evet Uygulamanın herkese açık URL'si. Yönlendirme URI'si bundan oluşturulur. Yalnızca localhost, 127.0.0.1 veya [::1] için http
SESSION_SECRET evet En az 32 karakter; oturum çerezini imzalar. openssl rand -base64 32 ile üretin
API_URL evet Uygulamanın çağırdığı API, örneğin http://localhost:8080
KEENAUTH_API_AUDIENCE evet Bu API'nin KeenAuth'taki tanımlayıcısı

app/lib/config.server.ts içindeki loadConfig, bu değerleri ilk istekte denetler. Eksik veya kullanılamaz bir değer isteği durdurur ve hiçbir hata iletisi bir değer içermez:

  const issuer = url("KEENAUTH_ISSUER", false);
  const appBaseUrl = url("APP_BASE_URL", true).origin;
  const sessionSecret = need("SESSION_SECRET");
  if (sessionSecret.length < 32) throw new Error("SESSION_SECRET must be at least 32 characters");

.server.ts ile biten dosyalar tarayıcı paketine hiçbir zaman ulaşmaz; bir sayfa bunları tarayıcı için içe aktarırsa React Router derlemeyi durdurur.

3. OpenID Connect istemcisini oluşturma

app/lib/keenauth.server.ts, istemciyi keşfden (<issuer>/.well-known/openid-configuration) bir kez oluşturur:

export function keenauth(): Promise<client.Configuration> {
  discovered ??= client
    .discovery(config().issuer, config().clientId, undefined, client.ClientSecretBasic(config().clientSecret), {
      // Also check the ID token signature with your environment's keys (jwks_uri), not only the TLS connection.
      execute: [client.enableNonRepudiationChecks],
    })

Varsayılan olarak openid-client, TLS üzerinden doğrudan belirteç uç noktasından gelen bir ID belirtecine güvenir. enableNonRepudiationChecks, bu belirtecin imzasını ortamınızın yayımladığı anahtarlarla da denetler.

4. Oturumu sunucuda tutma

Oturum sunucudaki dosyalarda yaşar; çerez yalnızca imzalı bir oturum kimliği tutar:

  storage ??= createFileSessionStorage<SessionData>({
    dir: ".sessions",
    cookie: {
      name: "__keenauth_session",
      httpOnly: true,
      sameSite: "lax",
      // Secure whenever the app is served over https; plain http only for localhost.
      secure: config().redirectUri.startsWith("https:"),
      path: "/",
      maxAge: 8 * 60 * 60,
      secrets: [config().sessionSecret],
    },
  });

Dosya deposu tek bir sunucuya hizmet eder. Birden çok sunucuda, createSessionStorage ile ve Redis gibi paylaşılan bir depoyla bir oturum deposu yazın. Belirteçler için çerez oturum deposunu kullanmayın: iki belirteç 4 KB çerez sınırını aşabilir ve tarayıcı bunları tutmuş olur.

5. Girişi başlatma

Rotalar app/routes.ts içinde listelenir:

export default [
  index("routes/home.tsx"),
  route("dashboard", "routes/dashboard.tsx"),
  route("login", "routes/login.ts"),
  route("auth/callback", "routes/callback.ts"),
  route("logout", "routes/logout.ts"),
] satisfies RouteConfig;

Giriş, /login adresine gönderen bir formdur:

        <Form method="post" action="/login" reloadDocument>
          <button type="submit">Sign in</button>
        </Form>

Action, her giriş için yeni bir state, nonce ve PKCE doğrulayıcısı oluşturur ve bunları oturumda tutar:

export async function action({ request }: Route.ActionArgs) {
  assertSameOrigin(request);
  const session = await getSession(request);
  const transaction = newTransaction();
  session.set("transaction", transaction);
  const url = await authorizationUrl(transaction);
  return redirect(url.href, { headers: { "Set-Cookie": await sessions().commitSession(session) } });
}

Yetkilendirme isteği, doğrulayıcının S256 aşamasını ve API'nizin tanımlayıcısını resource (RFC 8707) olarak gönderir. resource olmadan erişim belirteci API'niz için düzenlenmez ve API onu reddeder.

  return client.buildAuthorizationUrl(await keenauth(), {
    redirect_uri: config().redirectUri,
    // messages:read is a scope of your API; the example API requires it on /api/messages.
    scope: "openid profile email messages:read",
    state: transaction.state,
    nonce: transaction.nonce,
    // PKCE (RFC 7636): S256 of the verifier that only this session knows.
    code_challenge: await client.calculatePKCECodeChallenge(transaction.codeVerifier),
    code_challenge_method: "S256",
    // RFC 8707: an access token for your API.
    resource: config().apiAudience,
  });

POST isteğini yalnızca kendi sayfalarınızdan kabul etme

React Router, bir sayfanın action'ına siteler arası gelen POST isteğini reddeder; ancak /login ve /logout kaynak rotalarıdır (sayfası olmayan action'lar) ve bunlar için denetim sizindir. assertSameOrigin, POST isteğinin Origin başlığı (yoksa Referer) APP_BASE_URL adresini gösterdiğinde kabul eder:

  const app = new URL(config().redirectUri).origin;
  const source = request.headers.get("Origin") ?? request.headers.get("Referer");

6. Geri dönüşü işleme

Geri dönüş işlemi önce işlemi oturumdan alır; böylece bir state yalnızca bir kez çalışır:

  const old = await getSession(request);
  const transaction = old.get("transaction");
  // The transaction works once: it leaves the session before anything else happens.
  old.unset("transaction");

authorizationCodeGrant, ortamınızın her yetkilendirme yanıtında gönderdiği state ve iss değerlerini denetler (RFC 9207), kodu PKCE doğrulayıcısıyla değiştirir ve ID belirtecini denetler: imzası, iss, aud, exp, iat ve bu girişin nonce değeri. Yönlendirme URI'si APP_BASE_URL değerinden oluşturulur, asla Host başlığından değil:

  const callbackUrl = new URL(config().redirectUri);
  callbackUrl.search = new URL(request.url).search;
  return client.authorizationCodeGrant(await keenauth(), callbackUrl, {
    pkceCodeVerifier: transaction.codeVerifier,
    expectedState: transaction.state,
    expectedNonce: transaction.nonce,
    idTokenExpected: true,
  });

Başarısız bir denetim sabit bir hata koduyla 401 yanıtı verir. Başarılı girişten sonra uygulama eski oturumu siler ve kullanıcıyı ile belirteçleri yeni bir oturumda tutar:

  // A new session id after sign-in prevents session fixation: the old session is deleted, a new one is created.
  await sessions().destroySession(old);
  const session = await sessions().getSession(null);

7. Sayfaları koruma ve API'nizi çağırma

Bir kullanıcı gerektiren loader, requireUser işlevini çağırır; bu işlev oturum yoksa kullanıcıyı ana sayfaya yönlendirir. Örnekte yenileme belirteci yoktur: erişim belirteci süresi dolduğunda kullanıcı yeniden giriş yapar.

export async function loader({ request }: Route.LoaderArgs) {
  const { user, accessToken } = await requireUser(request);
  const api = await fetchMessages(accessToken);
  return { user, api };
}

Loader, API'nizi erişim belirteciyle taşıyıcı belirteç (bearer token) olarak çağırır. Belirteç sunucuda kalır; sayfa yalnızca kullanıcıyı ve iletileri alır.

    response = await fetch(`${config().apiUrl}/api/messages`, {
      headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
    });

8. Çıkış yapma

Çıkış, bu uygulamanın bir sayfasından yapılan bir POST isteğidir. Action yerel oturumu sonlandırır ve tarayıcıyı, uygulamanıza geri döndüren id_token_hint, client_id ve post_logout_redirect_uri parametreleriyle ortamınızın çıkış uç noktasına (OpenID Connect RP-Initiated Logout) gönderir:

  const session = await getSession(request);
  const idToken = session.get("idToken");
  const destroyed = await sessions().destroySession(session);
  return redirect((await logoutUrl(idToken)).href, { headers: { "Set-Cookie": destroyed } });

9. Uygulamayı çalıştırma

Express hızlı başlangıcındaki API'yi (ya da belirteçleri kabul eden herhangi bir API'yi) başlatın ve uygulamayı çalıştırın:

npm run dev

http://localhost:5173 adresini açın, Giriş yap seçeneğini seçin, KeenAuth'ta giriş yapın ve kontrol panelini açın.

10. Ağ olmadan test etme

Testler ağ bağlantısı ve KeenAuth hesabı gerektirmez. npm test uygulamayı derler ve derlemeyi React Router'ın istek işleyicisi üzerinden çalıştırır. Sahte bir fetch, ortamınızı (keşif, test çalıştırması için oluşturulmuş bir anahtar kümesi, belirteç uç noktası) ve API'yi canlandırır.

test("callback: an unknown or reused state is refused before any token request and starts no session", async () => {
  const b = browser();
  await startSignIn(b);
  const wrong = await returnFromKeenAuth(b, { state: "not-the-state" });
npm test
npm run deps    # exact pins, lockfile sources and licences

Testler şunları denetler: yapılandırmanın doğrulandığını (ve hiçbir hata iletisinde gizli bilginin bulunmadığını); girişin PKCE S256, state, nonce ve resource gönderdiğini ve başka bir siteden ya da GET ile gelen isteğin reddedildiğini; kodun eşleşen doğrulayıcı ve istemci kimlik doğrulamasıyla değiştirildiğini; erişim belirtecinin Authorization başlığında API'ye gittiğini ve asla tarayıcıya gitmediğini; bilinmeyen veya yeniden kullanılan bir state, eksik veya yanlış bir iss, bir yetkilendirme hatası ve başka bir istemciye, başka bir nonce ile veya başka bir anahtarla imzalanmış bir ID belirtecinin oturum olmadan reddedildiğini; girişin yeni bir oturum kimliği başlattığını; oturumu olmayan bir sayfanın ana sayfaya yönlendirildiğini; ve çıkışın başka bir siteden reddedildiğini ve id_token_hint ile çıkış uç noktasına gittiğini.

Gerçek bir tarayıcıyla tam giriş bir KeenAuth ortamı gerektirir. Bu, çevrimdışı testlerin bir parçası değildir.

Proxy veya yük dengeleyicinin arkasında

APP_BASE_URL değerini uygulamanın herkese açık URL'sine ayarlayın; yönlendirme URI'si ve Origin denetimi bunu kullanır, asla Host başlığını kullanmaz. Bir proxy TLS'yi sonlandırıyorsa APP_BASE_URL yine https URL'si olur, böylece oturum çerezi Secure kalır.

Bugünkü sınırlar

Sonraki adımlar