package authentication import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "ruben/inventory2/internal/consts" "time" "github.com/jackc/pgx/v5" ) // NewState creates a new state for logging in, saving it in the database. func (a *Authenticator) NewState(ctx context.Context) ([32]byte, error) { state, err := generateRandomState() if err != nil { return state, fmt.Errorf("failed to generate random state: %w", err) } if _, err = a.db.Exec( ctx, "INSERT INTO oauth_login_states (state) VALUES (@state)", pgx.NamedArgs{ "state": state[:], }, ); err != nil { return state, fmt.Errorf("failed to execute query: %w", err) } return state, nil } func generateRandomState() ([32]byte, error) { var b [32]byte _, err := rand.Read(b[:]) return b, err } // GetStateExpiration get's the oauth state's expiration // func (a *Authenticator) GetStateExpiration(ctx context.Context, state [32]byte) (time.Time, error) { func (a *Authenticator) GetStateExpiration(ctx context.Context, state string) (time.Time, error) { rows, err := a.db.Query( ctx, `SELECT expiration from oauth_login_states WHERE state = ('\x' || @state)::BYTEA`, pgx.NamedArgs{ //"state": state[:], "state": state, }, ) if err != nil { return time.Time{}, fmt.Errorf("failed to perform query: %w", err) } exp, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[time.Time]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return time.Time{}, consts.ErrNotFound } return time.Time{}, fmt.Errorf("failed to scan row: %w", err) } return exp, nil } // TODO: need to automatically clean up expired tokens func (a *Authenticator) DeleteOAuthTokens(ctx context.Context, accessToken string) error { _, err := a.db.Exec(ctx, "DELETE FROM oauth_tokens WHERE access_token = @access_token", pgx.NamedArgs{"access_token": accessToken}) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } return nil } func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, accessToken string) (claims AccessTokenClaims, expiration time.Time, err error) { rows, err := a.db.Query( ctx, ` SELECT expiry, claims FROM oauth_tokens WHERE access_token = @access_token `, pgx.NamedArgs{ "access_token": accessToken, }, ) if err != nil { return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to perform query: %w", err) } type Row struct { Expiry time.Time Claims json.RawMessage } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return AccessTokenClaims{}, time.Time{}, consts.ErrNotFound } return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan row: %w", err) } if err := json.Unmarshal(r.Claims, &claims); err != nil { return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan claims json: %w", err) } return claims, r.Expiry, nil }