用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/kemalcr/kemal --skill kemal-oauth命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Use when building, reviewing, debugging, testing, securing, or deploying web applications and HTTP APIs with the Kemal framework for Crystal. Covers Kemal routing, params, context, routers, filters, middleware, ECR, WebSockets, SSE, uploads, configuration, testing, and production concerns.
User authentication and session management in Kemal, following established project patterns.
Core Kemal development (routing verbs, parameters, modular router, version gates, response helpers).
正在显示 SKILL.md
| name | kemal-oauth |
| description | Implementing OAuth2 authentication in Kemal, following established project patterns. |
| license | MIT |
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.
Dependencies: The route examples below use env.session and env.flash, both provided by 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:
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:
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:
# 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:
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
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
env.flash below is kemal-session's one-time flash message helper (see Dependencies above).
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
GithubOauth).env.session for state management throughout the OAuth lifecycle. Always clean up state after verification.HTTP::Client with appropriate Authorization: Bearer headers.