Skip to content

Commit d5b684b

Browse files
fredbiclaude
andauthored
docs(security): document constant-time-comparison contract for auth callbacks (#457)
The runtime delegates credential comparison to caller-supplied authentication callbacks (UserPassAuthentication, TokenAuthentication, ScopedTokenAuthentication, and their Ctx variants). Callers that compare a secret against a known value must use crypto/subtle.ConstantTimeCompare to avoid response-timing side-channels. This commit: - Documents the contract on all six callback type godocs. - Updates the apikey and basic auth examples to demonstrate the safe pattern, so users copying the snippets inherit it. No API change. The runtime itself does not compare secrets and remains structurally timing-safe. Signed-off-by: Frederic BIDON <fredbi@yahoo.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1e4d68e commit d5b684b

3 files changed

Lines changed: 35 additions & 8 deletions

File tree

‎docs/examples/auth/apikey/main.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package main
1212

1313
import (
14+
"crypto/subtle"
1415
"log"
1516
"net/http"
1617
"os"
@@ -40,7 +41,9 @@ func wireAPIKeyAuth() {
4041
api.RegisterAuth("key", security.APIKeyAuth(
4142
"X-Token", "header",
4243
func(token string) (any, error) {
43-
if token == "abcdefuvwxyz" {
44+
// Use subtle.ConstantTimeCompare to avoid leaking the
45+
// expected token byte-by-byte via response timing.
46+
if subtle.ConstantTimeCompare([]byte(token), []byte("abcdefuvwxyz")) == 1 {
4447
return "alice", nil
4548
}
4649
return nil, errors.New(http.StatusUnauthorized, "invalid api key")

‎docs/examples/auth/basic/main.go‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package main
1010

1111
import (
1212
"context"
13+
"crypto/subtle"
1314
"net/http"
1415

1516
"github.com/go-openapi/errors"
@@ -33,7 +34,10 @@ type fakePrincipal struct{ Name string }
3334
type fakeStore struct{}
3435

3536
func (fakeStore) AuthenticateBasic(_ context.Context, user, pass string) (*fakePrincipal, error) {
36-
if user == "alice" && pass == "s3cret" {
37+
// subtle.ConstantTimeCompare avoids leaking the expected password
38+
// byte-by-byte via response timing. The username is non-secret and
39+
// compared with `==` purely to short-circuit unknown accounts.
40+
if user == "alice" && subtle.ConstantTimeCompare([]byte(pass), []byte("s3cret")) == 1 {
3741
return &fakePrincipal{Name: user}, nil
3842
}
3943
return nil, errors.Unauthenticated("basic")

‎security/authenticator.go‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,42 @@ func ScopedAuthenticator(handler func(*ScopedAuthRequest) (bool, any, error)) ru
4242
})
4343
}
4444

45-
// UserPassAuthentication authentication function.
45+
// UserPassAuthentication validates a basic-auth credential.
46+
//
47+
// Implementations comparing the password (or any derived secret) against a
48+
// known value MUST use [crypto/subtle.ConstantTimeCompare]: the runtime
49+
// extracts the credential from the request and delegates the comparison
50+
// here, and does not enforce a constant-time posture on the caller's behalf.
4651
type UserPassAuthentication func(string, string) (any, error)
4752

48-
// UserPassAuthenticationCtx authentication function with [context.Context].
53+
// UserPassAuthenticationCtx is the [context.Context]-aware variant of
54+
// [UserPassAuthentication]. The same constant-time-comparison guidance
55+
// applies.
4956
type UserPassAuthenticationCtx func(context.Context, string, string) (context.Context, any, error)
5057

51-
// TokenAuthentication authentication function.
58+
// TokenAuthentication validates an API-key token.
59+
//
60+
// Implementations comparing the token against a known value MUST use
61+
// [crypto/subtle.ConstantTimeCompare]; the runtime delegates the comparison
62+
// here and does not enforce a constant-time posture on the caller's behalf.
5263
type TokenAuthentication func(string) (any, error)
5364

54-
// TokenAuthenticationCtx authentication function with [context.Context].
65+
// TokenAuthenticationCtx is the [context.Context]-aware variant of
66+
// [TokenAuthentication]. The same constant-time-comparison guidance
67+
// applies.
5568
type TokenAuthenticationCtx func(context.Context, string) (context.Context, any, error)
5669

57-
// ScopedTokenAuthentication authentication function.
70+
// ScopedTokenAuthentication validates a bearer/OAuth2 token along with the
71+
// scopes required for the operation.
72+
//
73+
// Implementations comparing the token against a known value MUST use
74+
// [crypto/subtle.ConstantTimeCompare]; the runtime delegates the comparison
75+
// here and does not enforce a constant-time posture on the caller's behalf.
5876
type ScopedTokenAuthentication func(string, []string) (any, error)
5977

60-
// ScopedTokenAuthenticationCtx authentication function with [context.Context].
78+
// ScopedTokenAuthenticationCtx is the [context.Context]-aware variant of
79+
// [ScopedTokenAuthentication]. The same constant-time-comparison guidance
80+
// applies.
6181
type ScopedTokenAuthenticationCtx func(context.Context, string, []string) (context.Context, any, error)
6282

6383
var DefaultRealmName = "API"

0 commit comments

Comments
 (0)