Skip to main content

kemal-oauth

Implementing OAuth2 authentication in Kemal, following established project patterns.

Informations de source

Dépôt
kemalcr/kemal
Dernière activité de la source
21 août 2026 à 14:44
Langue détectée de SKILL.md
anglais
Étoiles
3 924
Forks
206

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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.
Voir sur GitHub