mcp connection
This commit is contained in:
130
go-api/internal/oauth/pkce.go
Normal file
130
go-api/internal/oauth/pkce.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Package oauth is KROW's OAuth 2.1 authorization server and the token store
|
||||
// behind it.
|
||||
//
|
||||
// It exists for one caller: the MCP surface, which needs a way to authenticate
|
||||
// a client that cannot hold a cookie. Everything here is in service of turning
|
||||
// a browser-based approval into an opaque bearer token that
|
||||
// mcpserver.TokenAuthenticator can resolve back into the SAME
|
||||
// authctx.Identity the cookie path produces.
|
||||
//
|
||||
// # WHAT THIS PACKAGE DOES NOT DO
|
||||
//
|
||||
// It does not authorize anything. It establishes WHO is calling; what they may
|
||||
// then read is decided by the existing policy table, in the existing tool
|
||||
// layer, exactly as it is for a cookie session. There is no OAuth scope that
|
||||
// grants access to a row. `krow.read` says "this client may use the read
|
||||
// tools"; whether this user may see a particular row is a question
|
||||
// tools/scope.go answers and this package never touches.
|
||||
//
|
||||
// It also does not store a password, check one, or keep a second user table.
|
||||
// The authorization endpoint authenticates the person using the session they
|
||||
// already have — see authserver.go.
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// PKCE — Proof Key for Code Exchange, RFC 7636.
|
||||
//
|
||||
// The problem it solves: an authorization code travels back through a browser
|
||||
// redirect, which is the least trustworthy hop in the flow. Anything that can
|
||||
// observe that redirect — a malicious app registered for the same custom URL
|
||||
// scheme, a proxy, a shoulder — can steal the code. For a confidential client
|
||||
// that does not matter, because redeeming the code also requires a client
|
||||
// secret. A public client has no secret, so the code alone would be enough.
|
||||
//
|
||||
// PKCE gives the client a per-request secret instead. It invents a random
|
||||
// `code_verifier`, sends only SHA-256 of it with the authorization request, and
|
||||
// presents the verifier itself at the token endpoint. A stolen code is useless
|
||||
// without the verifier, which never travelled through the browser.
|
||||
//
|
||||
// S256 ONLY. RFC 7636 also defines `plain`, where the challenge IS the
|
||||
// verifier. That protects against nothing — anyone who stole the code from the
|
||||
// redirect also stole the challenge, and the challenge is the verifier — and
|
||||
// OAuth 2.1 forbids it for public clients. It is refused here, and refused
|
||||
// again by a CHECK constraint in migration 000013, so no code path can relax it.
|
||||
|
||||
// MethodS256 is the only code_challenge_method this server accepts.
|
||||
const MethodS256 = "S256"
|
||||
|
||||
var (
|
||||
// ErrUnsupportedChallengeMethod covers `plain` and anything else.
|
||||
ErrUnsupportedChallengeMethod = errors.New("oauth: code_challenge_method must be S256")
|
||||
|
||||
// ErrMalformedChallenge covers a challenge that is not base64url of a
|
||||
// SHA-256 digest.
|
||||
ErrMalformedChallenge = errors.New("oauth: malformed code_challenge")
|
||||
|
||||
// ErrMalformedVerifier covers a verifier outside RFC 7636's length or
|
||||
// character set.
|
||||
ErrMalformedVerifier = errors.New("oauth: malformed code_verifier")
|
||||
|
||||
// ErrVerifierMismatch is the one that matters: a verifier that does not
|
||||
// hash to the stored challenge.
|
||||
ErrVerifierMismatch = errors.New("oauth: code_verifier does not match code_challenge")
|
||||
)
|
||||
|
||||
// challengePattern is base64url of a 32-byte digest: 43 characters, unpadded.
|
||||
// The same pattern migration 000013 enforces in oauth_grants_challenge_shape.
|
||||
var challengePattern = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
|
||||
|
||||
// verifierPattern is RFC 7636 section 4.1's `code_verifier` grammar:
|
||||
// unreserved characters only, 43 to 128 of them.
|
||||
var verifierPattern = regexp.MustCompile(`^[A-Za-z0-9._~-]{43,128}$`)
|
||||
|
||||
// ValidateChallenge checks a code_challenge and its method at the authorization
|
||||
// endpoint, before any row is written.
|
||||
//
|
||||
// Rejecting a malformed challenge here rather than at the token endpoint means
|
||||
// the failure lands where the client can act on it — on its own authorization
|
||||
// request — instead of after a person has been walked through a consent screen
|
||||
// for a flow that was never going to complete.
|
||||
func ValidateChallenge(challenge, method string) error {
|
||||
if method != MethodS256 {
|
||||
return ErrUnsupportedChallengeMethod
|
||||
}
|
||||
if !challengePattern.MatchString(challenge) {
|
||||
return ErrMalformedChallenge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyChallenge reports whether a verifier matches a stored challenge.
|
||||
//
|
||||
// The comparison is constant-time. A byte-by-byte comparison that returned
|
||||
// early would leak, through timing, how much of a guessed verifier was correct
|
||||
// — which turns an infeasible search into a feasible one, one character at a
|
||||
// time. The values being compared are both base64url text of the same fixed
|
||||
// length, so subtle.ConstantTimeCompare is exactly the right tool.
|
||||
func VerifyChallenge(verifier, challenge, method string) error {
|
||||
if method != MethodS256 {
|
||||
return ErrUnsupportedChallengeMethod
|
||||
}
|
||||
if !verifierPattern.MatchString(verifier) {
|
||||
return ErrMalformedVerifier
|
||||
}
|
||||
if !challengePattern.MatchString(challenge) {
|
||||
return ErrMalformedChallenge
|
||||
}
|
||||
|
||||
computed := ChallengeFor(verifier)
|
||||
if subtle.ConstantTimeCompare([]byte(computed), []byte(challenge)) != 1 {
|
||||
return ErrVerifierMismatch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChallengeFor derives the S256 challenge for a verifier.
|
||||
//
|
||||
// base64url WITHOUT padding, per RFC 7636 appendix A. Padding would add a '='
|
||||
// that has to be escaped in a query string, and a client that padded would
|
||||
// produce a challenge this server did not recognise.
|
||||
func ChallengeFor(verifier string) string {
|
||||
sum := sha256.Sum256([]byte(verifier))
|
||||
return base64.RawURLEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
Reference in New Issue
Block a user