Skip to main content

kemal-oauth

Implementing OAuth2 authentication in Kemal, following established project patterns.

ソース情報

リポジトリ
kemalcr/kemal
ソースの最終更新活動
2026年8月21日 14:44
検出された SKILL.md の言語
英語
スター
3,923
フォーク
206

インストール方法

デフォルトでは、最初にソースを確認する 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で見る