Documentation
¶
Index ¶
- Constants
- Variables
- func CheckPassword(password, hash string) bool
- func GenerateHash(password string) (string, error)
- func GenerateUserMac(userID string, serverSecret string) string
- func HashOauth2CodeVerifier(cv, secret string) string
- func HashOtp(otp, secret string) string
- func NewJwt(payload jwt.MapClaims, signingKey []byte, duration time.Duration) (string, error)
- func NewJwtEmailOtpToken(email, secret string, duration time.Duration) (otp string, token string, err error)
- func NewJwtEmailVerificationToken(userID, email, passwordHash, secret string, duration time.Duration) (string, error)
- func NewJwtOauth2StateToken(codeVerifier, secret string, duration time.Duration) (string, error)
- func NewJwtPasswordResetToken(userID, email, passwordHash, secret string, duration time.Duration) (string, error)
- func NewJwtSessionToken(userID, email, passwordHash, secret string, duration time.Duration) (string, error)
- func NewJwtSigningKeyWithCredentials(email, passwordHash, secret string) ([]byte, error)
- func Nonce() string
- func Oauth2CodeVerifier() string
- func Oauth2State() string
- func ParseJwt(token string, verificationKey []byte) (jwt.MapClaims, error)
- func ParseJwtUnverified(tokenString string) (jwt.MapClaims, error)
- func RandomNumericOTP() string
- func RandomString(length int, alphabet string) string
- func S256Challenge(code string) string
- func ValidateCodeVerifier(s string) error
- func ValidateEmailOtpClaims(claims jwt.MapClaims) error
- func ValidateEmailVerificationClaims(claims jwt.MapClaims) error
- func ValidatePasswordResetClaims(claims jwt.MapClaims) error
- func ValidateSessionClaims(claims jwt.MapClaims) error
- func VerifyEmailOtpToken(userOtp, tokenString, secret string) (string, error)
- func VerifyOauth2StateToken(tokenString, codeVerifier, secret string) error
- func VerifyUserMac(userID, providedMac, serverSecret string) bool
Constants ¶
const ( // MinKeyLength is the minimum required length for JWT signing keys. // 32 bytes (256 bits) is the minimum recommended length for HMAC-SHA256 keys // to provide sufficient security against brute force attacks. MinKeyLength = 32 // JWT claim constants ClaimIssuedAt = "iat" // JWT Issued At claim key ClaimExpiresAt = "exp" // JWT Expiration Time claim key ClaimUserID = "user_id" // JWT User ID claim key // Stateless cryptographic proof of the user_id ClaimUidMac = "uid_mac" // JWT User ID MAC claim key // Email verification specific claims ClaimEmail = "email" // Email address being verified ClaimType = "type" // Verification type claim ClaimVerificationValue = "verification" // Value for verification type claim ClaimPasswordResetValue = "password_reset" // Value for password reset type claim // OTP verification specific claims ClaimEmailOtpHash = "otp_hash" // SHA256 hash of the OTP code ClaimEmailOtpValue = "otp" // Value for OTP verification type claim // MaxTokenAge is the maximum age a JWT token can be before it's considered too old (7 days in seconds) MaxTokenAge = 7 * 24 * 60 * 60 )
const AlphanumericAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
const ClaimOauth2CodeVerifierHash = "cv_hash"
const ClaimOauth2StateValue = "oauth2_state"
const Oauth2StateLength = 32
The OAuth2 specification (RFC 6749) doesn’t mandate a specific length. It recommends a random, unguessable string. At least 16 characters, though 32 to 64 characters is common for better uniqueness and security.
const OauthCodeVerifierLength = 43
Defined in RFC 7636 (PKCE). Its length must be between 43 and 128 characters.
const PKCECodeChallengeMethod = "S256"
PKCE code challenge method as defined in RFC 7636
Variables ¶
var ( // ErrJwtTokenExpired is returned when the token has expired ErrJwtTokenExpired = errors.New("token expired") // ErrJwtInvalidToken is returned when the token is invalid ErrJwtInvalidToken = errors.New("invalid token") // ErrInvalidEmailOtpToken is returned when email OTP verification token is invalid ErrInvalidEmailOtpToken = errors.New("invalid email otp token") // ErrInvalidVerificationToken is returned when verification token is invalid ErrInvalidVerificationToken = errors.New("invalid verification token") // ErrJwtInvalidSigningMethod is returned when the signing method is not HS256 ErrJwtInvalidSigningMethod = errors.New("unexpected signing method") // ErrJwtInvalidSecretLength is returned for invalid secret lengths ErrJwtInvalidSecretLength = errors.New("invalid secret length") // ErrInvalidSigningKeyParts is returned when email or password hash are empty ErrInvalidSigningKeyParts = errors.New("invalid signing key parts") // ErrTokenUsedBeforeIssued is returned when a token's "iat" (issued at) claim // is in the future, indicating the token is being used before it was issued ErrTokenUsedBeforeIssued = errors.New("token used before issued") // ErrInvalidClaimFormat is returned when a claim has the wrong type ErrInvalidClaimFormat = errors.New("invalid claim format") // ErrClaimNotFound is returned when a required claim is missing ErrClaimNotFound = errors.New("claim not found") // ErrTokenTooOld is returned when a token's "iat" (issued at) claim // is older than the maximum allowed age (one week) ErrTokenTooOld = errors.New("token too old") )
var DummyPasswordHash = "$2a$10$5kebOn7bqUSaEWKNMUzJ2elZSgL.od24R.S1TiFTUWXYapS2ILPDe"
DummyPasswordHash is a precomputed bcrypt hash used exclusively to equalise response time on the not-found path of credential handlers. The plaintext it was derived from is intentionally unknown and irrelevant — CheckPassword against this hash will always return false, which is the desired behaviour.
package main
import (
"fmt"
"github.com/caasmo/restinpieces/crypto"
)
func main() {
hash, err := crypto.GenerateHash("restinpieces-qeq99qt")
if err != nil {
panic(err)
}
fmt.Println(hash)
}
the dummy hash must be generated with the same cost as the production one generated cost 10
var ( // ErrInvalidCodeVerifier is returned when a PKCE code_verifier is malformed. ErrInvalidCodeVerifier = errors.New("invalid code verifier") )
Functions ¶
func CheckPassword ¶
CheckPassword compares a bcrypt hashed password with its possible plaintext equivalent
func GenerateHash ¶
GenerateHash creates a bcrypt hash from a password using reasonable default cost bcrypt cost 10 is the historical default but is considered low by current standards. The cost is exponential — each increment doubles the work: Cost 10: ~100ms on a modern server Cost 12: ~400ms The tradeoff is direct: higher cost protects your stored hashes if the DB is ever leaked (slows offline cracking), but increases CPU load per login and worsens the DoS surface on your unauthenticated endpoint The current OWASP recommendation is cost 12 as a reasonable baseline for bcrypt. Cost 10 is becoming weak against modern GPU cracking rigs if your DB leaks.
func GenerateUserMac ¶ added in v0.10.0
GenerateUserMac creates a fast, stateless cryptographic proof that a user_id was generated by this server. We truncate the hex output to 16 characters (64 bits). 64 bits is plenty for online-only HTTP verification and keeps the JWT payload small.
func HashOauth2CodeVerifier ¶ added in v0.11.0
HashOauth2CodeVerifier creates an HMAC-SHA256 hash of the PKCE code_verifier. We use HMAC with the server's secret instead of a plain SHA256 to prevent length-extension attacks and to ensure the hash cannot be computed offline without the server's key.
func NewJwtEmailOtpToken ¶ added in v0.11.0
func NewJwtEmailVerificationToken ¶
func NewJwtEmailVerificationToken(userID, email, passwordHash, secret string, duration time.Duration) (string, error)
NewJwtEmailVerificationToken creates a JWT specifically for email verification
func NewJwtOauth2StateToken ¶ added in v0.11.0
NewJwtOauth2StateToken creates a stateless JWT state token that cryptographically binds the authorization flow to a specific code_verifier generated for the client.
# Architecture Details & Purpose The state token serves as an absolute protection mechanism against Confused Deputy (Outbound DoS) and Login CSRF attacks.
Even if the OAuth Provider does not support PKCE, we unconditionally use the code_verifier as a high-entropy (43-128 chars) client-side nonce. By embedding its hash inside this signed JWT, our backend ensures that any incoming /auth-with-oauth2 request originated from the exact same client session that requested the providers list.
This approach is 100% stateless (no database hits required) and IP-agnostic.
func NewJwtPasswordResetToken ¶
func NewJwtPasswordResetToken(userID, email, passwordHash, secret string, duration time.Duration) (string, error)
NewJwtPasswordResetToken creates a JWT specifically for password reset
func NewJwtSessionToken ¶
func NewJwtSessionToken(userID, email, passwordHash, secret string, duration time.Duration) (string, error)
NewJwtSession creates a new JWT session token for a user
func NewJwtSigningKeyWithCredentials ¶
It derives a unique key by combining user-specific data (email, passwordHash) with a server secret (JWT_SECRET). Tokens are invalidated when the user's email or password changes, or globally by rotating JWT_SECRET.
The passwordHash parameter can be empty to support passwordless authentication methods like OAuth2. In this case, the signing key is derived only from the email and server secret.
Using HMAC prevents length-extension attacks, unlike simple hash concatenation.
The function uses a null byte (\x00) as a delimiter to prevent collisions between the email and passwordHash inputs. It returns the key as a byte slice, suitable for use with github.com/golang-jwt/jwt/v5's SignedString method, and an error if the server secret is unset or inputs are invalid.
Note: JWT_SECRET should be a strong, random value (e.g., 32+ bytes).
func Nonce ¶ added in v0.13.0
func Nonce() string
Nonce generates a cryptographically secure random nonce suitable for use in Content Security Policy (CSP) script-src and style-src directives.
Standards and Requirements ¶
## W3C Content Security Policy Level 2 (https://www.w3.org/TR/CSP2/) ## W3C Content Security Policy Level 3 (https://www.w3.org/TR/CSP3/)
- Section 2.4 defines a nonce as a base64-encoded cryptographic random value.
- The nonce attribute value in HTML must be valid base64; alphanumeric or hex encodings are not conforming.
- The same nonce value must appear in both the CSP response header and the corresponding <script> or <style> tag attribute: Header: Content-Security-Policy: script-src 'nonce-<value>' Element: <script nonce="<value>">
## OWASP Content Security Policy Cheat Sheet (https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html)
- Minimum 128 bits of entropy required; 256 bits recommended.
- Must be generated fresh for every HTTP response. A nonce reused across responses is semantically equivalent to no nonce — it collapses into a static allowlist that any attacker can predict.
- Must never appear in URLs, logs, or any location other than the CSP header and the matching HTML attribute.
## NIST SP 800-90A (https://csrc.nist.gov/publications/detail/sp/800-90a/rev-1/final)
- Mandates a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator) as the source of randomness. crypto/rand.Read() satisfies this requirement by reading from the OS entropy source (/dev/urandom on Linux, CNG on Windows).
Implementation Notes ¶
32 bytes (256 bits) are read directly from crypto/rand into a raw byte slice. This avoids big.Int, which is designed for arithmetic on arbitrarily large integers and carries unnecessary overhead when the goal is simply filling a buffer with random bytes.
base64.StdEncoding produces a 44-character string from 32 bytes, which is the encoding required by the CSP specification.
A failure from crypto/rand.Read is treated as an unrecoverable condition and causes a panic. This is consistent with the rest of this package and with Go stdlib conventions for CSPRNG failures: a broken entropy source means the security guarantees of the entire system are void, and continuing execution would be more dangerous than crashing.
func Oauth2CodeVerifier ¶
func Oauth2CodeVerifier() string
func Oauth2State ¶
func Oauth2State() string
The state parameter helps prevent Cross-Site Request Forgery (CSRF) attacks by linking the authorization request to its callback. Should be URL-safe, Here alphanumeric characters.
func ParseJwt ¶
ParseJwt verifies and parses JWT and returns its claims. returns a map map[string]any that you can access like any other Go map.
func ParseJwtUnverified ¶
Implement only the validation rather than using the full validator but this is not lightweight either, 60% so expensive as full.
func RandomNumericOTP ¶ added in v0.10.0
func RandomNumericOTP() string
func RandomString ¶
func S256Challenge ¶
S256Challenge creates base64 encoded sha256 challenge string derived from code. The padding of the result base64 string is stripped per RFC 7636.
func ValidateCodeVerifier ¶ added in v0.11.0
ValidateCodeVerifier reports whether s is a well-formed PKCE code_verifier as defined by RFC 7636 §4.1: 43–128 characters from the PKCE alphabet. ValidateCodeVerifier checks s is a well-formed PKCE code_verifier per RFC 7636 §4.1.
func ValidateEmailOtpClaims ¶ added in v0.11.0
func ValidateSessionClaims ¶
func VerifyEmailOtpToken ¶ added in v0.11.0
func VerifyOauth2StateToken ¶ added in v0.11.0
VerifyOauth2StateToken parses the JWT state token and verifies that its embedded code_verifier hash perfectly matches the hash of the provided code_verifier. It fails if the token is expired, tampered with, or if the code_verifier is mismatched.
func VerifyUserMac ¶ added in v0.10.0
VerifyUserMac checks if the provided MAC matches the userID. CRITICAL: Uses subtle.ConstantTimeCompare to prevent timing attacks on the MAC itself.
Types ¶
This section is empty.