mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00

* feat(auth): implement oidc * refactor(auth): centralize OIDC state cookie handling * fix(web): resolve unused error variables in route handlers * docs(readme): add OIDC authentication feature to list * fix(auth): improve OIDC cookie handling for reverse proxy setups The OIDC state cookie's Secure flag is now properly set when running behind a reverse proxy by checking both direct TLS and X-Forwarded-Proto header. This fixes authentication issues in common setups where: - autobrr runs behind a reverse proxy that terminates HTTPS - local development environments without TLS - mixed protocol environments (internal HTTP, external HTTPS) * fix: use crypt/random if argon2id fails * feat(auth): show both login options when user exists in db if user doesn't exist, e.g. canOnboard=true then we only show the OIDC button, since regular login makes no sense in that case If user does not exist in db and the user wants to create a local user, OIDC needs to be disabled first * feat(auth): improve OIDC provider initialization with discovery logging * revert(issuer): do not remove trailing slash * feat(auth): improve OIDC username resolution with additional claims * fix(auth): handle OIDC issuer URLs with and without trailing slashes When initializing the OIDC provider, automatically retry with/without trailing slash if the first attempt fails. - First attempts with original issuer URL - If fails with trailing slash, retries without - If fails without trailing slash, retries with * feat(oidc): add gorilla sessions store for secure state management Add gorilla sessions store to handle encrypted state cookies in OIDC flow, while removing redundant session validation checks Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com> * fix(auth): prevent duplicate OIDC state cookies for authenticated sessions Modify OIDC config handler to check for existing authenticated sessions before setting state cookie. Still returns OIDC enabled status to maintain UI state, but prevents unnecessary cookie creation for authenticated users. * feat(oidc): use random secret for temporary state cookies Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com> * feat(auth): add rate limiting to OIDC endpoints Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com> * fix(auth): validate OIDC authorization code presence in callback Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com> * fix(auth): properly handle OIDC session errors Improve error handling in OIDC login flow by properly handling cookie store session errors. Return HTTP 500 if session cannot be retrieved instead of silently continuing with potentially invalid state. Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com> * feat(auth): track and display authentication method for oidc and password logins * fix: tests * docs(readme): add environment variable section * go mod tidy * chore: log style and errors --------- Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com> Co-authored-by: ze0s <ze0s@riseup.net>
174 lines
5.9 KiB
Go
174 lines
5.9 KiB
Go
// Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors.
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
package argon2id
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidHash in returned by ComparePasswordAndHash if the provided
|
|
// hash isn't in the expected format.
|
|
ErrInvalidHash = errors.New("argon2id: hash is not in the correct format")
|
|
|
|
// ErrIncompatibleVersion in returned by ComparePasswordAndHash if the
|
|
// provided hash was created using a different version of Argon2.
|
|
ErrIncompatibleVersion = errors.New("argon2id: incompatible version of argon2")
|
|
)
|
|
|
|
// DefaultParams provides some sane default parameters for hashing passwords.
|
|
//
|
|
// Follows recommendations given by the Argon2 RFC:
|
|
// "The Argon2id variant with t=1 and maximum available memory is RECOMMENDED as a
|
|
// default setting for all environments. This setting is secure against side-channel
|
|
// attacks and maximizes adversarial costs on dedicated bruteforce hardware.""
|
|
//
|
|
// The default parameters should generally be used for development/testing purposes
|
|
// only. Custom parameters should be set for production applications depending on
|
|
// available memory/CPU resources and business requirements.
|
|
var DefaultParams = &Params{
|
|
Memory: 64 * 1024,
|
|
Iterations: 1,
|
|
Parallelism: 2,
|
|
SaltLength: 16,
|
|
KeyLength: 32,
|
|
}
|
|
|
|
// Params describes the input parameters used by the Argon2id algorithm. The
|
|
// Memory and Iterations parameters control the computational cost of hashing
|
|
// the password. The higher these figures are, the greater the cost of generating
|
|
// the hash and the longer the runtime. It also follows that the greater the cost
|
|
// will be for any attacker trying to guess the password. If the code is running
|
|
// on a machine with multiple cores, then you can decrease the runtime without
|
|
// reducing the cost by increasing the Parallelism parameter. This controls the
|
|
// number of threads that the work is spread across. Important note: Changing the
|
|
// value of the Parallelism parameter changes the hash output.
|
|
//
|
|
// For guidance and an outline process for choosing appropriate parameters see
|
|
// https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4
|
|
type Params struct {
|
|
// The amount of memory used by the algorithm (in kibibytes).
|
|
Memory uint32
|
|
|
|
// The number of iterations over the memory.
|
|
Iterations uint32
|
|
|
|
// The number of threads (or lanes) used by the algorithm.
|
|
// Recommended value is between 1 and runtime.NumCPU().
|
|
Parallelism uint8
|
|
|
|
// Length of the random salt. 16 bytes is recommended for password hashing.
|
|
SaltLength uint32
|
|
|
|
// Length of the generated key. 16 bytes or more is recommended.
|
|
KeyLength uint32
|
|
}
|
|
|
|
// CreateHash returns a Argon2id hash of a plain-text password using the
|
|
// provided algorithm parameters. The returned hash follows the format used by
|
|
// the Argon2 reference C implementation and contains the base64-encoded Argon2id d
|
|
// derived key prefixed by the salt and parameters. It looks like this:
|
|
//
|
|
// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
|
|
func CreateHash(password string, params *Params) (hash string, err error) {
|
|
salt, err := GenerateRandomBytes(params.SaltLength)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
key := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength)
|
|
|
|
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
|
b64Key := base64.RawStdEncoding.EncodeToString(key)
|
|
|
|
hash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64Salt, b64Key)
|
|
return hash, nil
|
|
}
|
|
|
|
// ComparePasswordAndHash performs a constant-time comparison between a
|
|
// plain-text password and Argon2id hash, using the parameters and salt
|
|
// contained in the hash. It returns true if they match, otherwise it returns
|
|
// false.
|
|
func ComparePasswordAndHash(password, hash string) (match bool, err error) {
|
|
match, _, err = CheckHash(password, hash)
|
|
return match, err
|
|
}
|
|
|
|
// CheckHash is like ComparePasswordAndHash, except it also returns the params that the hash was
|
|
// created with. This can be useful if you want to update your hash params over time (which you
|
|
// should).
|
|
func CheckHash(password, hash string) (match bool, params *Params, err error) {
|
|
params, salt, key, err := DecodeHash(hash)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
otherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength)
|
|
|
|
keyLen := int32(len(key))
|
|
otherKeyLen := int32(len(otherKey))
|
|
|
|
if subtle.ConstantTimeEq(keyLen, otherKeyLen) == 0 {
|
|
return false, params, nil
|
|
}
|
|
if subtle.ConstantTimeCompare(key, otherKey) == 1 {
|
|
return true, params, nil
|
|
}
|
|
return false, params, nil
|
|
}
|
|
|
|
func GenerateRandomBytes(n uint32) ([]byte, error) {
|
|
b := make([]byte, n)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// DecodeHash expects a hash created from this package, and parses it to return the params used to
|
|
// create it, as well as the salt and key (password hash).
|
|
func DecodeHash(hash string) (params *Params, salt, key []byte, err error) {
|
|
vals := strings.Split(hash, "$")
|
|
if len(vals) != 6 {
|
|
return nil, nil, nil, ErrInvalidHash
|
|
}
|
|
|
|
var version int
|
|
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
if version != argon2.Version {
|
|
return nil, nil, nil, ErrIncompatibleVersion
|
|
}
|
|
|
|
params = &Params{}
|
|
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Iterations, ¶ms.Parallelism)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
params.SaltLength = uint32(len(salt))
|
|
|
|
key, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
params.KeyLength = uint32(len(key))
|
|
|
|
return params, salt, key, nil
|
|
}
|