save oauth user_id and link users and accounts
This commit is contained in:
@@ -2,7 +2,6 @@ package authentication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -82,90 +81,7 @@ func New(ctx context.Context, db *pgxpool.Pool) (*Authenticator, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken.
|
||||
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("no id_token field in oauth2 token")
|
||||
}
|
||||
|
||||
oidcConfig := &oidc.Config{
|
||||
ClientID: a.ClientID,
|
||||
}
|
||||
|
||||
return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: AUTH0_DOMAIN,
|
||||
Path: "/v2/logout",
|
||||
RawQuery: url.Values{
|
||||
"returnTo": {
|
||||
(&url.URL{
|
||||
Scheme: "https",
|
||||
Host: requestHost,
|
||||
}).String(),
|
||||
},
|
||||
"client_id": {AUTH0_CLIENT_ID},
|
||||
}.Encode(),
|
||||
}
|
||||
}
|
||||
|
||||
// Exchange exchanges an auth code for an access token.
|
||||
func (a *Authenticator) Exchange(ctx context.Context, state, code string) (accessToken string, expiration time.Time, err error) {
|
||||
// validate state
|
||||
|
||||
@@ -193,8 +109,6 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
|
||||
|
||||
fmt.Println("ID TOKEN:", idToken)
|
||||
|
||||
// claims []byte
|
||||
|
||||
var claims map[string]any
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("Failed to obtain id token claims: %w", err)
|
||||
@@ -204,9 +118,30 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
|
||||
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
|
||||
// store the token, and the potentially new user
|
||||
|
||||
if _, err := a.db.Exec(
|
||||
ctx,
|
||||
`
|
||||
WITH new_user AS (
|
||||
INSERT INTO oauth_users (
|
||||
user_id
|
||||
)
|
||||
VALUES (
|
||||
@id_token_subject
|
||||
)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING
|
||||
user_id
|
||||
), the_user AS (
|
||||
SELECT
|
||||
COALESCE(user_id, user_id_2) as user_id
|
||||
FROM
|
||||
new_user
|
||||
RIGHT JOIN
|
||||
(SELECT @id_token_subject as user_id_2)
|
||||
ON TRUE
|
||||
)
|
||||
INSERT INTO oauth_tokens (
|
||||
access_token,
|
||||
token_type,
|
||||
@@ -221,9 +156,9 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
|
||||
id_token_nonce,
|
||||
id_token_access_token_hash,
|
||||
|
||||
claims
|
||||
claims -- might want to open this up
|
||||
)
|
||||
VALUES (
|
||||
SELECT
|
||||
@access_token,
|
||||
@token_type,
|
||||
@refresh_token,
|
||||
@@ -231,14 +166,15 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
|
||||
|
||||
@id_token_issuer,
|
||||
@id_token_audience,
|
||||
@id_token_subject,
|
||||
user_id,
|
||||
@id_token_expiry,
|
||||
@id_token_issued_at,
|
||||
@id_token_nonce,
|
||||
@id_token_access_token_hash,
|
||||
|
||||
@claims
|
||||
)
|
||||
FROM
|
||||
the_user
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"access_token": token.AccessToken,
|
||||
@@ -263,51 +199,33 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
|
||||
return token.AccessToken, token.Expiry.UTC(), 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)
|
||||
// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken.
|
||||
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("no id_token field in oauth2 token")
|
||||
}
|
||||
return nil
|
||||
|
||||
oidcConfig := &oidc.Config{
|
||||
ClientID: a.ClientID,
|
||||
}
|
||||
|
||||
return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
|
||||
}
|
||||
|
||||
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)
|
||||
func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: AUTH0_DOMAIN,
|
||||
Path: "/v2/logout",
|
||||
RawQuery: url.Values{
|
||||
"returnTo": {
|
||||
(&url.URL{
|
||||
Scheme: "https",
|
||||
Host: requestHost,
|
||||
}).String(),
|
||||
},
|
||||
"client_id": {AUTH0_CLIENT_ID},
|
||||
}.Encode(),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user