Skip to main content

kemal-oauth

Implementing OAuth2 authentication in Kemal, following established project patterns.

معلومات المصدر

المستودع
kemalcr/kemal
آخر نشاط في المصدر
٢١ أغسطس ٢٠٢٦ في ١٤:٤٤
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٣٬٩٢٣
التفرعات
٢٠٦

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
kemal-oauth
description
Implementing OAuth2 authentication in Kemal, following established project patterns.
license
MIT
# Kemal OAuth2 Integration This skill provides expert guidance on integrating OAuth2 authentication (e.g., GitHub, Google) into Kemal applications, strictly following patterns from [`kemal-by-example/oauth-login`](https://github.com/sdogruyol/kemal-by-example/tree/master/oauth-login). ## Core Mandates - **Dependencies:** The route examples below use `env.session` and `env.flash`, both provided by [`kemal-session`](https://github.com/kemalcr/kemal-session) (`env.flash` since kemal-session 1.4.0). Add it to `shard.yml` and `require "kemal-session"`. - **Configuration:** Use environment variables for client IDs and secrets: ```crystal def client_id : String ENV["GITHUB_CLIENT_ID"]? || "" end def client_secret : String ENV["GITHUB_CLIENT_SECRET"]? || "" end def redirect_uri : String ENV["OAUTH_REDIRECT_URI"]? || "http://127.0.0.1:3000/auth/github/callback" end ``` - **Authorization URL:** Use `URI::Params` to construct the authorization URL with required scopes and a state parameter: ```crystal def authorize_url(state : String) : String params = URI::Params.build do |form| form.add "client_id", client_id form.add "redirect_uri", redirect_uri form.add "scope", "read:user user:email" form.add "state", state end "https://github.com/login/oauth/authorize?#{params}" end ``` - **State Parameter:** Generate state with `Random::Secure.random_bytes(16).hexstring`, store in `env.session`, verify in callback, then delete: ```crystal # In authorization route: state = Random::Secure.random_bytes(16).hexstring env.session.string("oauth_state", state) # In callback: stored = env.session.string?("oauth_state") env.session.delete_string("oauth_state") halt env.status(:forbidden) unless state == stored ``` - **Exchanging Code:** Use `HTTP::Client` to exchange the authorization code for an access token. Always set `Accept`, `Content-Type`, and `User-Agent` headers: ```crystal response = HTTP::Client.post( "https://github.com/login/oauth/access_token", headers: HTTP::Headers{ "Accept" => "application/json", "Content-Type" => "application/x-www-form-urlencoded", "User-Agent" => "my-app", }, body: URI::Params.encode({ "client_id" => client_id, "client_secret" => client_secret, "code" => code, "redirect_uri" => redirect_uri, }) ) return nil unless response.success? json = JSON.parse(response.body) json["access_token"]?.try(&.as_s?) rescue JSON::ParseException nil ``` ## Patterns from Source Code ### OAuth Service Module (oauth-login/src/services/github_oauth.cr) ```crystal require "http/client" require "json" require "uri" module OauthLogin module GithubOauth extend self USER_AGENT = "kemal-oauth-login" def client_id : String ENV["GITHUB_CLIENT_ID"]? || "" end def client_secret : String ENV["GITHUB_CLIENT_SECRET"]? || "" end def redirect_uri : String ENV["OAUTH_REDIRECT_URI"]? || "http://127.0.0.1:3000/auth/github/callback" end def configured? : Bool !client_id.empty? && !client_secret.empty? end def authorize_url(state : String) : String params = URI::Params.build do |form| form.add "client_id", client_id form.add "redirect_uri", redirect_uri form.add "scope", "read:user user:email" form.add "state", state end "https://github.com/login/oauth/authorize?#{params}" end def exchange_code(code : String) : String? body = URI::Params.encode({ "client_id" => client_id, "client_secret" => client_secret, "code" => code, "redirect_uri" => redirect_uri, }) response = HTTP::Client.post( "https://github.com/login/oauth/access_token", headers: HTTP::Headers{ "Accept" => "application/json", "Content-Type" => "application/x-www-form-urlencoded", "User-Agent" => USER_AGENT, }, body: body ) return nil unless response.success? json = JSON.parse(response.body) json["access_token"]?.try(&.as_s?) rescue JSON::ParseException nil end def fetch_github_user(access_token : String) : Hash(String, JSON::Any)? response = HTTP::Client.get( "https://api.github.com/user", headers: HTTP::Headers{ "Authorization" => "Bearer #{access_token}", "Accept" => "application/vnd.github+json", "User-Agent" => USER_AGENT, } ) return nil unless response.success? JSON.parse(response.body).as_h? rescue JSON::ParseException nil end end end ``` ### OAuth Routes (oauth-login/src/routes/oauth.cr) `env.flash` below is kemal-session's one-time flash message helper (see Dependencies above). ```crystal require "random" get "/auth/github" do |env| unless OauthLogin::GithubOauth.configured? env.flash["error"] = "Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET. See README." env.redirect "/" next end state = Random::Secure.random_bytes(16).hexstring env.session.string("oauth_state", state) env.redirect OauthLogin::GithubOauth.authorize_url(state) end get "/auth/github/callback" do |env| code = env.params.query["code"]? state = env.params.query["state"]? stored = env.session.string?("oauth_state") env.session.delete_string("oauth_state") unless code && state && stored && state == stored env.flash["error"] = "OAuth state mismatch or missing code." env.redirect "/" next end token = OauthLogin::GithubOauth.exchange_code(code) unless token env.flash["error"] = "Could not exchange code for token." env.redirect "/" next end # ... fetch user, sign in, redirect end ``` ## Best Practices - **Service Isolation:** Encapsulate OAuth logic within dedicated service modules (e.g., `GithubOauth`). - **Error Handling:** Gracefully handle API errors and JSON parsing failures during the OAuth flow using specific exception types. - **Session Security:** Use `env.session` for state management throughout the OAuth lifecycle. Always clean up state after verification. - **Profile Fetching:** Use the access token to fetch user profile information using `HTTP::Client` with appropriate `Authorization: Bearer` headers. - **Configured Check:** Check if OAuth is configured before redirecting to avoid errors. ## When to Use - When implementing "Login with GitHub/Google" features. - When interacting with external APIs that require OAuth2 authentication. - When managing OAuth flows and callbacks in a Kemal application.
عرض على GitHub