Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions OIDC_INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# OpenID Connect (OIDC) Integration

This document describes the OIDC integration added to pgbackweb to support authentication via external providers like Authentik, Keycloak, and other OIDC-compliant identity providers.

## Overview

The OIDC integration allows users to authenticate using external identity providers instead of local username/password combinations. This supports enterprise SSO workflows and centralized user management.

## Features

- Support for any OIDC-compliant identity provider
- Automatic user creation on first login
- User information synchronization on each login
- Seamless integration with existing authentication system
- Configurable user attribute mapping

## Configuration

Add the following environment variables to enable OIDC:

```bash
# Enable OIDC authentication
PBW_OIDC_ENABLED=true

# OIDC Provider Configuration
PBW_OIDC_ISSUER_URL=https://your-provider.com/auth/realms/your-realm
PBW_OIDC_CLIENT_ID=pgbackweb
PBW_OIDC_CLIENT_SECRET=your-client-secret
PBW_OIDC_REDIRECT_URL=https://your-domain.com/auth/oidc/callback

# Optional: Customize OIDC scopes (default: "openid profile email")
PBW_OIDC_SCOPES="openid profile email"

# Optional: Customize claim mappings
PBW_OIDC_USERNAME_CLAIM=preferred_username # default: preferred_username
PBW_OIDC_EMAIL_CLAIM=email # default: email
PBW_OIDC_NAME_CLAIM=name # default: name
```

## Provider-Specific Setup

### Authentik

1. Create a new **OAuth2/OpenID Provider** in Authentik
2. Set the **Redirect URI** to: `https://your-domain.com/auth/oidc/callback`
3. Configure the **Client Type** as **Confidential**
4. Note the **Client ID** and **Client Secret**
5. Create a new **Application** and link it to the provider
6. Configure the **Issuer URL**: `https://your-authentik.com/application/o/your-app/`

### Keycloak

1. Create a new **Client** in your Keycloak realm
2. Set **Client Protocol** to `openid-connect`
3. Set **Access Type** to `confidential`
4. Add `https://your-domain.com/auth/oidc/callback` to **Valid Redirect URIs**
5. Note the **Client ID** and get the **Client Secret** from the Credentials tab
6. Configure the **Issuer URL**: `https://your-keycloak.com/auth/realms/your-realm`

### Generic OIDC Provider

For any OIDC-compliant provider:

1. Create a new OIDC client/application
2. Set the redirect URI to: `https://your-domain.com/auth/oidc/callback`
3. Ensure the client can access `openid`, `profile`, and `email` scopes
4. Note the issuer URL (usually ends with `/.well-known/openid_configuration`)

## Database Schema Changes

The OIDC integration adds the following columns to the `users` table:

```sql
ALTER TABLE users
ADD COLUMN oidc_provider TEXT,
ADD COLUMN oidc_subject TEXT;

-- Make password nullable for OIDC users
ALTER TABLE users ALTER COLUMN password DROP NOT NULL;

-- Create unique index for OIDC users
CREATE UNIQUE INDEX users_oidc_provider_subject_idx
ON users (oidc_provider, oidc_subject)
WHERE oidc_provider IS NOT NULL AND oidc_subject IS NOT NULL;

-- Ensure users have either password or OIDC authentication
ALTER TABLE users ADD CONSTRAINT users_auth_method_check
CHECK (
(password IS NOT NULL AND oidc_provider IS NULL AND oidc_subject IS NULL) OR
(password IS NULL AND oidc_provider IS NOT NULL AND oidc_subject IS NOT NULL)
);
```

## User Flow

1. **First-time users**: When an OIDC user logs in for the first time, a new user account is automatically created with information from the OIDC provider.

2. **Returning users**: Existing OIDC users are matched by their provider and subject ID. User information (name, email) is updated from the OIDC provider on each login.

3. **Mixed authentication**: The system supports both local users (with passwords) and OIDC users in the same instance.

## Security Considerations

- **State parameter**: CSRF protection using a random state parameter
- **Token validation**: ID tokens are cryptographically verified
- **Secure cookies**: State is stored in secure, HTTP-only cookies
- **Provider validation**: Only configured OIDC providers are accepted

## User Interface

When OIDC is enabled, the login page displays:
- A "Login with SSO" button at the top
- A divider separating SSO from traditional login
- The existing email/password form below

## Implementation Details

### Services

- **`internal/service/oidc/`**: Core OIDC authentication logic
- **`internal/view/web/oidc/`**: Web routes for OIDC authentication flow
- **`internal/config/`**: Environment variable configuration and validation

### Routes

- `GET /auth/oidc/login`: Initiates OIDC authentication flow
- `GET /auth/oidc/callback`: Handles OIDC provider callback

### Database Queries

- `OIDCServiceCreateUser`: Creates a new OIDC user
- `OIDCServiceGetUserByOIDC`: Retrieves user by provider and subject
- `OIDCServiceUpdateUser`: Updates existing OIDC user information

## Troubleshooting

### Common Issues

1. **Invalid redirect URI**: Ensure the redirect URI in your OIDC provider matches exactly: `https://your-domain.com/auth/oidc/callback`

2. **Certificate errors**: If using self-signed certificates, ensure your Go application trusts the certificates

3. **Claim mapping**: Verify that your OIDC provider returns the expected claims (`email`, `name`, `preferred_username`)

4. **Scopes**: Ensure your OIDC client has access to the required scopes (`openid`, `profile`, `email`)

### Debug Logging

The application logs OIDC authentication events. Check logs for:
- OIDC provider initialization errors
- Token exchange failures
- User creation/update events

## Migration from Local Authentication

Existing local users are unaffected by OIDC integration. To migrate users to OIDC:

1. Enable OIDC authentication
2. Users can continue using local authentication or switch to OIDC
3. No automatic migration is performed - users choose their preferred method

## Development

To run the application with OIDC in development:

```bash
# Set environment variables in .env file
echo "PBW_OIDC_ENABLED=true" >> .env
echo "PBW_OIDC_ISSUER_URL=https://your-dev-provider.com" >> .env
# ... other OIDC variables

# Run database migrations
task migrate up

# Generate database code
task gen-db

# Build and run
task dev
```
5 changes: 4 additions & 1 deletion cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ func main() {
dbgen := dbgen.New(db)

ints := integration.New()
servs := service.New(env, dbgen, cr, ints)
servs, err := service.New(env, dbgen, cr, ints)
if err != nil {
logger.FatalError("error initializing services", logger.KV{"error": err})
}
initSchedule(cr, servs)

app := echo.New()
Expand Down
2 changes: 1 addition & 1 deletion cmd/changepw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func main() {
err = dbg.UsersServiceChangePassword(
context.Background(), dbgen.UsersServiceChangePasswordParams{
ID: userID,
Password: hashedPassword,
Password: sql.NullString{String: hashedPassword, Valid: true},
},
)
if err != nil {
Expand Down
15 changes: 9 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.49
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.3
github.com/caarlos0/env/v11 v11.3.1
github.com/coreos/go-oidc/v3 v3.14.1
github.com/go-co-op/gocron/v2 v2.11.0
github.com/go-playground/validator/v10 v10.22.0
github.com/google/uuid v1.6.0
Expand All @@ -21,9 +22,10 @@ require (
github.com/nodxdev/nodxgo-htmx v0.1.0
github.com/nodxdev/nodxgo-lucide v0.1.1
github.com/orsinium-labs/enum v1.4.0
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.25.0
golang.org/x/sync v0.7.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.36.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.12.0
)

require (
Expand All @@ -43,6 +45,7 @@ require (
github.com/aws/smithy-go v1.22.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
Expand All @@ -56,8 +59,8 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
32 changes: 20 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE=
github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
Expand All @@ -55,6 +59,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
Expand Down Expand Up @@ -97,28 +103,30 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
11 changes: 11 additions & 0 deletions internal/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ type Env struct {
PBW_POSTGRES_CONN_STRING string `env:"PBW_POSTGRES_CONN_STRING,required"`
PBW_LISTEN_HOST string `env:"PBW_LISTEN_HOST" envDefault:"0.0.0.0"`
PBW_LISTEN_PORT string `env:"PBW_LISTEN_PORT" envDefault:"8085"`

// OIDC Configuration
PBW_OIDC_ENABLED bool `env:"PBW_OIDC_ENABLED" envDefault:"false"`
PBW_OIDC_ISSUER_URL string `env:"PBW_OIDC_ISSUER_URL"`
PBW_OIDC_CLIENT_ID string `env:"PBW_OIDC_CLIENT_ID"`
PBW_OIDC_CLIENT_SECRET string `env:"PBW_OIDC_CLIENT_SECRET"`
PBW_OIDC_REDIRECT_URL string `env:"PBW_OIDC_REDIRECT_URL"`
PBW_OIDC_SCOPES string `env:"PBW_OIDC_SCOPES" envDefault:"openid profile email"`
PBW_OIDC_USERNAME_CLAIM string `env:"PBW_OIDC_USERNAME_CLAIM" envDefault:"preferred_username"`
PBW_OIDC_EMAIL_CLAIM string `env:"PBW_OIDC_EMAIL_CLAIM" envDefault:"email"`
PBW_OIDC_NAME_CLAIM string `env:"PBW_OIDC_NAME_CLAIM" envDefault:"name"`
}

var (
Expand Down
16 changes: 16 additions & 0 deletions internal/config/env_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,21 @@ func validateEnv(env Env) error {
return fmt.Errorf("invalid listen port %s, valid values are 1-65535", env.PBW_LISTEN_PORT)
}

// Validate OIDC configuration if enabled
if env.PBW_OIDC_ENABLED {
if env.PBW_OIDC_ISSUER_URL == "" {
return fmt.Errorf("PBW_OIDC_ISSUER_URL is required when OIDC is enabled")
}
if env.PBW_OIDC_CLIENT_ID == "" {
return fmt.Errorf("PBW_OIDC_CLIENT_ID is required when OIDC is enabled")
}
if env.PBW_OIDC_CLIENT_SECRET == "" {
return fmt.Errorf("PBW_OIDC_CLIENT_SECRET is required when OIDC is enabled")
}
if env.PBW_OIDC_REDIRECT_URL == "" {
return fmt.Errorf("PBW_OIDC_REDIRECT_URL is required when OIDC is enabled")
}
}

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE users
ADD COLUMN oidc_provider TEXT,
ADD COLUMN oidc_subject TEXT,
ADD COLUMN password_nullable TEXT;

-- Make password nullable and copy existing passwords
UPDATE users SET password_nullable = password;
ALTER TABLE users DROP COLUMN password;
ALTER TABLE users RENAME COLUMN password_nullable TO password;

-- Create unique index for OIDC users
CREATE UNIQUE INDEX users_oidc_provider_subject_idx
ON users (oidc_provider, oidc_subject)
WHERE oidc_provider IS NOT NULL AND oidc_subject IS NOT NULL;

-- Add constraint to ensure either password or OIDC is set
ALTER TABLE users ADD CONSTRAINT users_auth_method_check
CHECK (
(password IS NOT NULL AND oidc_provider IS NULL AND oidc_subject IS NULL) OR
(password IS NULL AND oidc_provider IS NOT NULL AND oidc_subject IS NOT NULL)
);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS users_oidc_provider_subject_idx;
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_auth_method_check;
ALTER TABLE users DROP COLUMN IF EXISTS oidc_provider;
ALTER TABLE users DROP COLUMN IF EXISTS oidc_subject;

-- Make password required again (this will fail if there are OIDC users)
ALTER TABLE users ALTER COLUMN password SET NOT NULL;
-- +goose StatementEnd
Comment on lines +33 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add safety check before making password NOT NULL in down migration.

The down migration will fail with a constraint violation if any OIDC users exist (users with null passwords). Consider adding a check or removing such users first.

 -- Make password required again (this will fail if there are OIDC users)
+-- First check if there are OIDC users
+DO $$
+BEGIN
+    IF EXISTS (SELECT 1 FROM users WHERE password IS NULL) THEN
+        RAISE EXCEPTION 'Cannot rollback: OIDC users exist with null passwords';
+    END IF;
+END $$;
 ALTER TABLE users ALTER COLUMN password SET NOT NULL;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- Make password required again (this will fail if there are OIDC users)
ALTER TABLE users ALTER COLUMN password SET NOT NULL;
-- +goose StatementEnd
-- Make password required again (this will fail if there are OIDC users)
-- First check if there are OIDC users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM users WHERE password IS NULL) THEN
RAISE EXCEPTION 'Cannot rollback: OIDC users exist with null passwords';
END IF;
END $$;
ALTER TABLE users ALTER COLUMN password SET NOT NULL;
-- +goose StatementEnd
🤖 Prompt for AI Agents
In internal/database/migrations/20250708000000_add_oidc_support_to_users.sql
around lines 33 to 35, the down migration attempts to set the password column as
NOT NULL without checking for existing users with null passwords, causing
constraint violations. To fix this, add a safety check before altering the
column by either deleting or updating users with null passwords to valid values,
or conditionally applying the NOT NULL constraint only if no null passwords
exist.

Loading
Loading