├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── golangci-lint.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .golangci.yml ├── .idea ├── .gitignore ├── modules.xml ├── vcs.xml └── vigilo.iml ├── .releaserc.json ├── .vscode ├── jsconfig.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── cmd ├── config │ ├── application │ │ ├── application_config.go │ │ └── config.yaml │ ├── audit │ │ └── audit_config_yaml.go │ ├── login │ │ └── login_config_yaml.go │ ├── password │ │ └── password_config_yaml.go │ ├── server │ │ └── server_config_yaml.go │ ├── smtp │ │ └── smtp_config_yaml.go │ └── token │ │ └── token_config_yaml.go └── identity-server │ └── main.go ├── docs ├── contributing │ └── README.md └── user_guide │ ├── README.md │ ├── configuration │ ├── configuration_guide.md │ └── docker.md │ └── identity │ ├── README.md │ └── endpoints │ ├── admin_handler │ └── get_audit_events.md │ ├── authz_handler │ └── authorize_client.md │ ├── client_handler │ ├── client_delete_request.md │ ├── client_read_request.md │ ├── client_registration.md │ ├── client_secret_regeneration.md │ └── client_update_request.md │ ├── consent_handler │ └── user_consent.md │ ├── oidc_handler │ ├── discovery.md │ ├── jwks.md │ └── user_info.md │ ├── token_handler │ ├── client_credentials_grant.md │ ├── ropc_grant.md │ ├── token_exchange.md │ ├── token_introspection.md │ ├── token_refresh.md │ └── token_revocation.md │ └── user_handler │ ├── basic_user_authentication.md │ ├── oauth_user_authentication.md │ ├── password_reset.md │ ├── password_reset_request.md │ ├── user_logout.md │ ├── user_registration.md │ └── verify.md ├── go.mod ├── go.sum ├── go.work.sum ├── idp ├── config │ ├── audit_config.go │ ├── logging_config.go │ ├── login_config.go │ ├── password_config.go │ ├── server_config.go │ ├── smtp_config.go │ └── token_config.go └── server │ └── identity_server.go ├── internal ├── background │ ├── audit_jobs.go │ ├── audit_jobs_test.go │ ├── scheduler.go │ ├── scheduler_test.go │ ├── smtp_jobs.go │ ├── smtp_jobs_test.go │ ├── token_jobs.go │ ├── token_jobs_test.go │ ├── user_jobs.go │ └── user_jobs_test.go ├── constants │ ├── application_types.go │ ├── audit.go │ ├── claims.go │ ├── context.go │ ├── displays.go │ ├── email.go │ ├── env.go │ ├── grant_types.go │ ├── http.go │ ├── ids.go │ ├── oidc.go │ ├── prompts.go │ ├── response_types.go │ ├── roles.go │ └── timeouts.go ├── container │ ├── container.go │ ├── db_registry.go │ ├── handler_registry.go │ ├── lazy_init.go │ ├── scheduler_registry.go │ ├── server_config_registry.go │ └── service_registry.go ├── domain │ ├── audit │ │ ├── model.go │ │ ├── repository.go │ │ └── service.go │ ├── authorization │ │ └── service.go │ ├── authzcode │ │ ├── creator.go │ │ ├── issuer.go │ │ ├── model.go │ │ ├── repository.go │ │ ├── service.go │ │ ├── validation.go │ │ └── validator.go │ ├── claims │ │ └── model.go │ ├── client │ │ ├── authenticator.go │ │ ├── authorization.go │ │ ├── creator.go │ │ ├── manager.go │ │ ├── model.go │ │ ├── repository.go │ │ └── validator.go │ ├── cookies │ │ └── service.go │ ├── crypto │ │ └── cryptographer.go │ ├── email │ │ ├── mailer.go │ │ ├── model.go │ │ └── service.go │ ├── jwks │ │ └── model.go │ ├── jwt │ │ └── service.go │ ├── login │ │ ├── repository.go │ │ └── service.go │ ├── oidc │ │ ├── model.go │ │ └── service.go │ ├── session │ │ ├── manager.go │ │ ├── model.go │ │ ├── repository.go │ │ └── service.go │ ├── token │ │ ├── creator.go │ │ ├── grant_processor.go │ │ ├── issuer.go │ │ ├── manager.go │ │ ├── model.go │ │ ├── parser.go │ │ ├── repository.go │ │ ├── validation.go │ │ └── validator.go │ ├── user │ │ ├── authenticator.go │ │ ├── creator.go │ │ ├── manager.go │ │ ├── model.go │ │ ├── repository.go │ │ ├── validation.go │ │ └── verifier.go │ └── userconsent │ │ ├── model.go │ │ ├── repository.go │ │ └── service.go ├── errors │ ├── codes.go │ ├── collection.go │ └── vigilo_errors.go ├── handlers │ ├── admin_handler.go │ ├── authz_handler.go │ ├── client_handler.go │ ├── consent_handler.go │ ├── oidc_handler.go │ ├── token_handler.go │ └── user_handler.go ├── middleware │ ├── middleware.go │ ├── middleware_test.go │ ├── rate_limiter.go │ └── rate_limiter_test.go ├── mocks │ ├── audit │ │ ├── repository.go │ │ └── service.go │ ├── authorization │ │ └── service.go │ ├── authzcode │ │ ├── creator.go │ │ ├── issuer.go │ │ ├── manager.go │ │ ├── repository.go │ │ └── validator.go │ ├── client │ │ ├── authenticator.go │ │ ├── authorization.go │ │ ├── creator.go │ │ ├── manager.go │ │ ├── repository.go │ │ └── validator.go │ ├── cookies │ │ └── service.go │ ├── crypto │ │ └── cryptographer.go │ ├── email │ │ ├── mailer.go │ │ └── service.go │ ├── jwt │ │ └── service.go │ ├── login │ │ ├── repository.go │ │ └── service.go │ ├── session │ │ ├── manager.go │ │ ├── repository.go │ │ └── service.go │ ├── token │ │ ├── creator.go │ │ ├── grant_processor.go │ │ ├── issuer.go │ │ ├── manager.go │ │ ├── parser.go │ │ ├── repository.go │ │ └── validator.go │ ├── user │ │ ├── authenticator.go │ │ ├── creator.go │ │ ├── manager.go │ │ ├── repository.go │ │ └── verifier.go │ └── userconsent │ │ ├── repository.go │ │ └── service.go ├── repository │ ├── audit │ │ └── memory.go │ ├── authzcode │ │ └── memory.go │ ├── client │ │ ├── memory.go │ │ └── memory_test.go │ ├── login │ │ └── memory.go │ ├── session │ │ ├── memory.go │ │ └── memory_test.go │ ├── token │ │ ├── memory.go │ │ └── memory_test.go │ ├── user │ │ └── memory.go │ └── userconsent │ │ └── memory.go ├── routes │ ├── route_group.go │ ├── router_config.go │ └── routes.go ├── service │ ├── audit │ │ └── service.go │ ├── authorization │ │ ├── service.go │ │ └── service_test.go │ ├── authzcode │ │ ├── creator.go │ │ ├── creator_test.go │ │ ├── issuer.go │ │ ├── issuer_test.go │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── validator.go │ │ └── validator_test.go │ ├── client │ │ ├── authenticator.go │ │ ├── authenticator_test.go │ │ ├── authorization.go │ │ ├── authorization_test.go │ │ ├── creator.go │ │ ├── creator_test.go │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── validator.go │ │ └── validator_test.go │ ├── cookies │ │ ├── service.go │ │ └── service_test.go │ ├── crypto │ │ ├── cryptographer.go │ │ └── cryptographer_test.go │ ├── email │ │ ├── mailer.go │ │ ├── service.go │ │ ├── service_test.go │ │ └── templates │ │ │ ├── account_deletion.html │ │ │ └── account_verification.html │ ├── jwt │ │ └── service.go │ ├── login │ │ ├── service.go │ │ └── service_test.go │ ├── oidc │ │ ├── service.go │ │ └── service_test.go │ ├── session │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── service.go │ │ └── service_test.go │ ├── token │ │ ├── creator.go │ │ ├── creator_test.go │ │ ├── grant_processor.go │ │ ├── grant_processor_test.go │ │ ├── issuer.go │ │ ├── issuer_test.go │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── validator.go │ │ └── validator_test.go │ ├── user │ │ ├── authenticator.go │ │ ├── authenticator_test.go │ │ ├── creator.go │ │ ├── creator_test.go │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── verifier.go │ │ └── verifier_test.go │ └── userconsent │ │ ├── service.go │ │ └── service_test.go ├── types │ ├── client_types.go │ ├── code_challenge_methods.go │ ├── scopes.go │ ├── token_endpoint_auth_types.go │ └── token_types.go ├── utils │ ├── context.go │ ├── helpers.go │ └── logging.go └── web │ ├── decode_json.go │ ├── endpoints.go │ ├── header.go │ └── responses.go ├── scripts ├── push_docker_dev.sh ├── run_docker_local.sh └── update_imports.sh ├── tests └── integration │ ├── admin_handler_integration_test.go │ ├── authz_handler_integration_test.go │ ├── client_handler_integration_test.go │ ├── consent_handler_integration_test.go │ ├── oidc_handler_integration_test.go │ ├── rate_limiting_test.go │ ├── token_handler_integration_test.go │ ├── user_handler_integration_test.go │ └── vigilo_test_context.go └── ui ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── App.jsx ├── api │ ├── clientApi.js │ └── userApi.js ├── components │ ├── BaseButton.jsx │ ├── Container.jsx │ ├── FlexContainer.jsx │ └── FormInput.jsx ├── constants │ ├── endpoints.js │ └── urlParams.js ├── context │ └── ApplicationContext.jsx ├── forms │ ├── AuthenticationForm.jsx │ ├── ConsentForm.jsx │ └── InvalidRedirectErrorPage.jsx ├── index.css ├── index.js ├── pages │ ├── AuthenticationPage.jsx │ ├── ConsentPage.jsx │ └── ErrorPage.jsx ├── popups │ ├── AuthenticationPopup.jsx │ └── ConsentPopup.jsx └── styles │ ├── AuthenticationPage.scss │ ├── ConsentPage.scss │ ├── ErrorPage.scss │ ├── forms.scss │ └── popup.scss └── tailwind.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | A clear and concise description of what the bug is. 12 | 13 | --- 14 | 15 | ### Steps to reproduce 16 | 17 | --- 18 | 19 | ### Expected behavior 20 | A clear and concise description of what you expected to happen. 21 | 22 | --- 23 | 24 | ### Screenshots 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | --- 28 | 29 | ### Additional context 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | pull_request: 5 | branches: [ '*' ] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version-file: ./go.mod 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v8 21 | with: 22 | version: 'latest' 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ '*' ] 6 | 7 | jobs: 8 | run-tests: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: stable 19 | 20 | - name: Create .env.test file 21 | run: | 22 | echo "SMTP_USERNAME=${{ secrets.SMTP_USERNAME }}" >> .env.test 23 | echo "SMTP_FROM_ADDRESS=${{ secrets.SMTP_FROM_ADDRESS }}" >> .env.test 24 | echo "SMTP_PASSWORD=${{ secrets.SMTP_PASSWORD }}" >> .env.test 25 | echo "TOKEN_ISSUER=${{ secrets.TOKEN_ISSUER }}" >> .env.test 26 | echo "TOKEN_PRIVATE_KEY=${{ secrets.TOKEN_PRIVATE_KEY }}" >> .env.test 27 | echo "TOKEN_PUBLIC_KEY=${{ secrets.TOKEN_PUBLIC_KEY }}" >> .env.test 28 | echo "CRYPTO_SECRET_KEY=${{ secrets.CRYPTO_SECRET_KEY }}" >> .env.test 29 | 30 | - name: Install necessary dependencies 31 | run: go mod tidy 32 | 33 | - name: Format code 34 | run: gofmt -s -w . 35 | 36 | - name: Run tests with coverage 37 | run: go test -coverprofile=coverage.out ./internal/... ./tests/integration/... 38 | 39 | - name: Update coverage report 40 | uses: ncruces/go-coverage-report@v0 41 | with: 42 | report: true 43 | chart: true 44 | amend: true 45 | if: | 46 | github.event_name == 'pull_request' 47 | continue-on-error: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | /ui/node_modules/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | run: 4 | timeout: 5m 5 | 6 | linters: 7 | enable: 8 | - govet 9 | - errcheck 10 | - staticcheck 11 | - unused 12 | - ineffassign 13 | - bodyclose 14 | - contextcheck 15 | - containedctx 16 | - errorlint 17 | - gocognit 18 | - goconst 19 | - godox 20 | - maintidx 21 | - misspell 22 | - mnd 23 | - nestif 24 | - sqlclosecheck 25 | - testifylint 26 | - unparam 27 | - whitespace 28 | - wrapcheck 29 | 30 | formatters: 31 | enable: 32 | - gofmt 33 | - goimports 34 | 35 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vigilo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"], 3 | "plugins": [ 4 | ["@semantic-release/commit-analyzer", { 5 | "preset": "angular", 6 | "releaseRules": [ 7 | {"type": "breaking", "release": "major"}, 8 | {"type": "feat", "release": "minor"}, 9 | {"type": "fix", "release": "patch"}, 10 | 11 | {"type": "task", "release": false}, 12 | {"type": "test", "release": false}, 13 | {"type": "refactor", "release": false}, 14 | {"type": "perf", "release": false}, 15 | {"type": "docs", "release": false}, 16 | {"type": "chore", "release": false} 17 | ] 18 | }], 19 | "@semantic-release/release-notes-generator", 20 | "@semantic-release/github" 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "jsx": "react-jsx" 5 | }, 6 | "exclude": ["node_modules"] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "GORELEASER" 4 | ], 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "always" 7 | }, 8 | "go.formatTool": "goimports", 9 | "[go]": { 10 | "editor.defaultFormatter": "golang.go" 11 | }, 12 | "files.autoSave": "onFocusChange", 13 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", 14 | "editor.formatOnType": false, 15 | "editor.formatOnPaste": true, 16 | "editor.formatOnSaveMode": "file", 17 | "vs-code-prettier-eslint.prettierLast": false, 18 | "editor.formatOnSave": true, 19 | "eslint.format.enable": true, 20 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"] 21 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS ui-builder 2 | WORKDIR /app 3 | COPY ui/ ./ 4 | RUN npm install 5 | RUN npm run build 6 | 7 | FROM golang:latest AS builder 8 | WORKDIR /app 9 | COPY . . 10 | 11 | ENV GOOS=linux 12 | ENV GOARCH=amd64 13 | ENV CGO_ENABLED=0 14 | 15 | RUN go build -o /app/identity-server ./cmd/identity-server 16 | 17 | FROM alpine:latest 18 | WORKDIR /app 19 | COPY --from=builder /app/identity-server . 20 | COPY --from=builder /app/cmd/config/application/config.yaml ./config.yaml 21 | COPY --from=ui-builder /app/build ./ui/build 22 | 23 | RUN chmod +x ./identity-server 24 | EXPOSE 8080 25 | 26 | ENV VIGILO_SERVER_MODE=docker 27 | ENV REACT_BUILD_PATH=/app/ui/build 28 | 29 | CMD ["./identity-server"] 30 | -------------------------------------------------------------------------------- /cmd/config/application/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigiloauth/vigilo/80980e8b5728750c4df0453953fcbba44e9cf098/cmd/config/application/config.yaml -------------------------------------------------------------------------------- /cmd/config/audit/audit_config_yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/vigiloauth/vigilo/v2/idp/config" 7 | ) 8 | 9 | const oneDay time.Duration = 24 * time.Hour 10 | 11 | type AuditLogConfigYAML struct { 12 | RetentionPeriod *int `yaml:"retention_period,omitempty"` 13 | } 14 | 15 | func (alc *AuditLogConfigYAML) ToOptions() []config.AuditLogConfigOptions { 16 | options := []config.AuditLogConfigOptions{} 17 | 18 | if alc.RetentionPeriod != nil { 19 | retention := time.Duration(*alc.RetentionPeriod) * oneDay 20 | options = append(options, config.WithRetentionPeriod(retention)) 21 | } 22 | 23 | return options 24 | } 25 | -------------------------------------------------------------------------------- /cmd/config/login/login_config_yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/vigiloauth/vigilo/v2/idp/config" 7 | ) 8 | 9 | type LoginConfigYAML struct { 10 | MaxFailedAttempts *int `yaml:"max_failed_attempts,omitempty"` 11 | Delay *int64 `yaml:"delay,omitempty"` 12 | LoginURL *string `yaml:"login_url,omitempty"` 13 | } 14 | 15 | func (lc *LoginConfigYAML) ToOptions() []config.LoginConfigOptions { 16 | options := []config.LoginConfigOptions{} 17 | 18 | if lc.MaxFailedAttempts != nil { 19 | options = append(options, config.WithMaxFailedAttempts(*lc.MaxFailedAttempts)) 20 | } 21 | 22 | if lc.Delay != nil { 23 | delay := time.Duration(*lc.Delay) * time.Millisecond 24 | options = append(options, config.WithDelay(delay)) 25 | } 26 | 27 | if lc.LoginURL != nil { 28 | options = append(options, config.WithLoginURL(*lc.LoginURL)) 29 | } 30 | 31 | return options 32 | } 33 | -------------------------------------------------------------------------------- /cmd/config/password/password_config_yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/vigiloauth/vigilo/v2/idp/config" 4 | 5 | type PasswordConfigYAML struct { 6 | RequireUppercase *bool `yaml:"require_uppercase,omitempty"` 7 | RequireNumber *bool `yaml:"require_number,omitempty"` 8 | RequireSymbol *bool `yaml:"require_symbol,omitempty"` 9 | MinimumLength *int `yaml:"minimum_length,omitempty"` 10 | } 11 | 12 | func (pc *PasswordConfigYAML) ToOptions() []config.PasswordConfigOptions { 13 | options := []config.PasswordConfigOptions{} 14 | 15 | if pc.RequireUppercase != nil { 16 | options = append(options, config.WithUppercase()) 17 | } 18 | 19 | if pc.RequireNumber != nil { 20 | options = append(options, config.WithNumber()) 21 | } 22 | 23 | if pc.RequireSymbol != nil { 24 | options = append(options, config.WithSymbol()) 25 | } 26 | 27 | if pc.MinimumLength != nil { 28 | options = append(options, config.WithMinLength(*pc.MinimumLength)) 29 | } 30 | 31 | return options 32 | } 33 | -------------------------------------------------------------------------------- /cmd/config/server/server_config_yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/vigiloauth/vigilo/v2/idp/config" 7 | ) 8 | 9 | type ServerConfigYAML struct { 10 | Port *string `yaml:"port,omitempty"` 11 | CertFilePath *string `yaml:"cert_file_path,omitempty"` 12 | KeyFilePath *string `yaml:"key_file_path,omitempty"` 13 | SessionCookieName *string `yaml:"session_cookie_name,omitempty"` 14 | ForceHTTPS *bool `yaml:"force_https,omitempty"` 15 | Domain *string `yaml:"domain,omitempty"` 16 | EnableRequestLogging *bool `yaml:"enable_request_logging,omitempty"` 17 | 18 | ReadTimeout *int64 `yaml:"read_timeout,omitempty"` 19 | WriteTimeout *int64 `yaml:"write_timeout,omitempty"` 20 | AuthzCodeDuration *int64 `yaml:"authorization_code_duration,omitempty"` 21 | } 22 | 23 | func (sc *ServerConfigYAML) ToOptions() []config.ServerConfigOptions { 24 | options := []config.ServerConfigOptions{} 25 | 26 | if sc.Port != nil { 27 | options = append(options, config.WithPort(*sc.Port)) 28 | } 29 | if sc.CertFilePath != nil { 30 | options = append(options, config.WithCertFilePath(*sc.CertFilePath)) 31 | } 32 | if sc.KeyFilePath != nil { 33 | options = append(options, config.WithKeyFilePath(*sc.KeyFilePath)) 34 | } 35 | if sc.SessionCookieName != nil { 36 | options = append(options, config.WithSessionCookieName(*sc.SessionCookieName)) 37 | } 38 | if sc.ForceHTTPS != nil { 39 | options = append(options, config.WithForceHTTPS()) 40 | } 41 | if sc.EnableRequestLogging != nil { 42 | options = append(options, config.WithRequestLogging(*sc.EnableRequestLogging)) 43 | } 44 | if sc.ReadTimeout != nil { 45 | timeoutDuration := time.Duration(*sc.ReadTimeout) * time.Second 46 | options = append(options, config.WithReadTimeout(timeoutDuration)) 47 | } 48 | if sc.WriteTimeout != nil { 49 | timeoutDuration := time.Duration(*sc.WriteTimeout) * time.Second 50 | options = append(options, config.WithWriteTimeout(timeoutDuration)) 51 | } 52 | if sc.AuthzCodeDuration != nil { 53 | duration := time.Duration(*sc.AuthzCodeDuration) * time.Minute 54 | options = append(options, config.WithAuthorizationCodeDuration(duration)) 55 | } 56 | if sc.Domain != nil { 57 | options = append(options, config.WithDomain(*sc.Domain)) 58 | } 59 | 60 | return options 61 | } 62 | -------------------------------------------------------------------------------- /cmd/config/smtp/smtp_config_yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/vigiloauth/vigilo/v2/idp/config" 4 | 5 | const TLSPort int = 587 6 | const SSLPort int = 465 7 | 8 | type SMTPConfigYAML struct { 9 | Host *string `yaml:"host,omitempty"` 10 | Port *int `yaml:"port,omitempty"` 11 | Username *string `yaml:"username,omitempty"` 12 | Password *string `yaml:"password,omitempty"` 13 | FromAddress *string `yaml:"from_address,omitempty"` 14 | Encryption *string `yaml:"encryption,omitempty"` 15 | } 16 | 17 | func (s *SMTPConfigYAML) ToOptions() []config.SMTPConfigOptions { 18 | options := []config.SMTPConfigOptions{} 19 | 20 | if s.Host != nil { 21 | options = append(options, config.WithSMTPHost(*s.Host)) 22 | } 23 | 24 | if s.Port != nil { 25 | switch *s.Port { 26 | case TLSPort: 27 | options = append(options, config.WithTLS()) 28 | case SSLPort: 29 | options = append(options, config.WithSSL()) 30 | } 31 | } 32 | 33 | if s.Username != nil && s.Password != nil { 34 | options = append(options, config.WithCredentials(*s.Username, *s.Password)) 35 | } 36 | 37 | if s.FromAddress != nil { 38 | options = append(options, config.WithFromAddress(*s.FromAddress)) 39 | } 40 | 41 | if s.Encryption != nil { 42 | options = append(options, config.WithEncryption(*s.Encryption)) 43 | } 44 | 45 | return options 46 | } 47 | -------------------------------------------------------------------------------- /cmd/config/token/token_config_yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/vigiloauth/vigilo/v2/idp/config" 7 | ) 8 | 9 | const oneDay time.Duration = 24 * time.Hour 10 | 11 | type TokenConfigYAML struct { 12 | SecretKey *string `yaml:"secret_key,omitempty"` 13 | ExpirationTime *int64 `yaml:"expiration_time,omitempty"` 14 | AccessTokenDuration *int64 `yaml:"access_token_duration,omitempty"` 15 | RefreshTokenDuration *int64 `yaml:"refresh_token_duration,omitempty"` 16 | } 17 | 18 | func (tc *TokenConfigYAML) ToOptions() []config.TokenConfigOptions { 19 | options := []config.TokenConfigOptions{} 20 | 21 | if tc.ExpirationTime != nil { 22 | duration := time.Duration(*tc.ExpirationTime) * time.Minute 23 | options = append(options, config.WithExpirationTime(duration)) 24 | } 25 | 26 | if tc.AccessTokenDuration != nil { 27 | duration := time.Duration(*tc.AccessTokenDuration) * time.Minute 28 | options = append(options, config.WithAccessTokenDuration(duration)) 29 | } 30 | 31 | if tc.RefreshTokenDuration != nil { 32 | duration := time.Duration(*tc.RefreshTokenDuration) * (oneDay) 33 | options = append(options, config.WithRefreshTokenDuration(duration)) 34 | } 35 | 36 | return options 37 | } 38 | -------------------------------------------------------------------------------- /docs/user_guide/identity/endpoints/oidc_handler/discovery.md: -------------------------------------------------------------------------------- 1 | # Discovery Endpoint 2 | 3 | ## Endpoint 4 | ```http 5 | GET /identity/oauth2/.well-known/openid-configuration 6 | ``` 7 | 8 | ## Headers 9 | | Key | Value | Description | 10 | | :-------------- | :---------------------------- | :---------------------------------------- | 11 | | Content-Type | application/json | Indicates that the request body is JSON. | 12 | | Date | Tue, 03 Dec 2024 19:38:16 GMT | The date and time the request was made. | 13 | | Content-Length | [Content-Length] | The length of the request body in bytes. | 14 | 15 | --- 16 | 17 | ## Example Request 18 | ```http 19 | GET /identity/oauth2/.well-known/openid-configuration HTTP/1.1 20 | ``` 21 | 22 | ## Responses 23 | 24 | #### HTTP Status Code: `200 OK` 25 | #### Response Body: 26 | ```json 27 | { 28 | "issuer": "https://auth.example.com", 29 | "authorization_endpoint": "https://auth.example.com/oauth2/authorize", 30 | "token_endpoint": "https://auth.example.com/oauth2/token", 31 | "user_info_endpoint": "https://auth.example.com/oauth2/userinfo", 32 | "jwks_uri": "https://auth.example.com/oauth2/.well-known/jwks.json", 33 | "registration_endpoint": "https://auth.example.com/client/register", 34 | "scopes_supported": [ 35 | "clients:manage", 36 | "clients:read", 37 | "clients:write", 38 | "clients:delete", 39 | "users:manage", 40 | "users:read", 41 | "users:write", 42 | "users:delete", 43 | "tokens:revoke", 44 | "tokens:introspect", 45 | "oidc", 46 | "email", 47 | "address", 48 | "profile", 49 | "phone" 50 | "offline_access", 51 | ], 52 | "response_types_supported": [ 53 | "id_token", 54 | "code", 55 | "token" 56 | ], 57 | "grant_types_supported": [ 58 | "refresh_token", 59 | "implicit_flow", 60 | "password", 61 | "authorization_code", 62 | "pkce", 63 | "client_credentials", 64 | "device_code" 65 | ], 66 | "subject_types_supported": ["public"], 67 | "id_token_signing_alg_values_supported": ["RS256"], 68 | "id_token_encryption_alg_values_supported": ["RSA-OAEP"], 69 | "token_endpoint_auth_methods_supported": [ 70 | "client_secret_basic", 71 | "client_secret_post", 72 | "none" 73 | ] 74 | } 75 | ``` -------------------------------------------------------------------------------- /docs/user_guide/identity/endpoints/oidc_handler/jwks.md: -------------------------------------------------------------------------------- 1 | # JWKS Endpoint 2 | 3 | ## Endpoint 4 | ```http 5 | GET /identity/oauth2/.well-known/jwks.json 6 | ``` 7 | 8 | ## Headers 9 | | Key | Value | Description | 10 | | :-------------- | :---------------------------- | :---------------------------------------- | 11 | | Content-Type | application/json | Indicates that the request body is JSON. | 12 | | Date | Tue, 03 Dec 2024 19:38:16 GMT | The date and time the request was made. | 13 | | Content-Length | [Content-Length] | The length of the request body in bytes. | 14 | 15 | --- 16 | 17 | ## Example Request 18 | ```http 19 | GET /identity/oauth2/.well-known/jwks.json HTTP/1.1 20 | ``` 21 | 22 | ## Responses 23 | 24 | #### HTTP Status Code: `200 OK` 25 | #### Response Body: 26 | ```json 27 | { 28 | "keys": [ 29 | { 30 | "kty": "RSA", 31 | "kid": "abc123", 32 | "use": "sig", 33 | "alg": "RS256", 34 | "n": "modulus-base64url", 35 | "e": "exponent-base64url" 36 | } 37 | ] 38 | } 39 | ``` -------------------------------------------------------------------------------- /docs/user_guide/identity/endpoints/user_handler/basic_user_authentication.md: -------------------------------------------------------------------------------- 1 | # Basic User Authentication 2 | 3 | ## Endpoint 4 | ``` 5 | POST /identity/auth/login 6 | ``` 7 | 8 | --- 9 | 10 | ### Headers 11 | | Key | Value | Description | 12 | | :-------------- | :---------------------------- | :-----------------------------------------| 13 | | Content-Type | application/json | Indicates that the request body is JSON. | 14 | | Date | Tue, 03 Dec 2024 19:38:16 GMT | The date and time the request was made. | 15 | | Content-Length | [Content-Length] | The length of the request body in bytes. | 16 | 17 | --- 18 | 19 | ### Request Body 20 | | Field | Type | Required | Description | 21 | |:----------|:--------|:----------|:-------------------------------| 22 | | `email` | `string` | Yes | The user's email address. | 23 | | `password` | `string` | Yes | The password for the account. | 24 | 25 | --- 26 | 27 | ### Example Request 28 | ```json 29 | { 30 | "email": "john.doe@email.com", 31 | "password": "Pas$_w0rds" 32 | } 33 | ``` 34 | 35 | --- 36 | 37 | ## Responses 38 | #### HTTP Status Code: `200 OK` 39 | #### Response Body: 40 | ```json 41 | { 42 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", 43 | } 44 | ``` 45 | 46 | --- 47 | 48 | ## Error Responses 49 | ### 1. Missing Email Field 50 | #### HTTP Status Code: `404 Not Found` 51 | #### Response Body: 52 | ```json 53 | { 54 | "error": "user_not_found", 55 | "error_description": "user not found." 56 | } 57 | ``` 58 | 59 | ### 2. Invalid Password 60 | #### HTTP Status Code: `401 Unauthorized` 61 | #### Response Body: 62 | ```json 63 | { 64 | "error": "invalid_credentials", 65 | "error_description": "invalid Credentials" 66 | } 67 | ``` -------------------------------------------------------------------------------- /docs/user_guide/identity/endpoints/user_handler/user_logout.md: -------------------------------------------------------------------------------- 1 | # User Logout 2 | ## Endpoint 3 | ``` 4 | POST /identity/auth/logout 5 | ``` 6 | --- 7 | 8 | ### Headers 9 | | Key | Value | Description | 10 | | :-------------- | :---------------------------- | :----------------------------------------- | 11 | | Content-Type | application/json | Indicates that the request body is JSON. | 12 | | Date | Tue, 03 Dec 2024 19:38:16 GMT | The date and time the request was made. | 13 | | Content-Length | [Content-Length] | The length of the request body in bytes. | 14 | 15 | --- 16 | 17 | ## Example Request 18 | ```http 19 | POST /identity/auth/logout HTTP/1.1 20 | Authorization: Bearer reg-23410913-abewfq.123483 21 | ``` 22 | 23 | --- 24 | 25 | ## Responses 26 | #### HTTP Status Code: `200 OK` 27 | #### Response Headers: 28 | | Key | Value | 29 | |:----------------|-------------------------------| 30 | | Content-Type | application/json | 31 | | Date | Tue, 03 Dec 2024 19:38:16 GMT | 32 | --- 33 | 34 | ## Error Responses 35 | ### 1. Missing Authorization Header 36 | #### HTTP Status Code: `401 Unauthorized` 37 | #### Response Body: 38 | ```json 39 | { 40 | "error": "invalid_credentials", 41 | "error_description": "invalid credentials" 42 | } 43 | ``` 44 | 45 | ### 2. Invalid Token 46 | #### HTTP Status Code: `401 Unauthorized` 47 | #### Response Body: 48 | ```json 49 | { 50 | "error": "invalid_credentials", 51 | "error_description": "invalid credentials" 52 | } 53 | ``` -------------------------------------------------------------------------------- /docs/user_guide/identity/endpoints/user_handler/verify.md: -------------------------------------------------------------------------------- 1 | # Verify Account 2 | 3 | ## Endpoint 4 | ```http 5 | GET /identity/auth/verify 6 | ``` 7 | 8 | --- 9 | 10 | **Description:** 11 | This endpoint is used when a new user needs to verify their email. During the registration process, the user will receive an email to verify their account. 12 | 13 | --- 14 | 15 | ## Query Parameters 16 | | Parameter | Type | Required | Description | 17 | :-----------------|:------------|:------------|:--------------------------------------------------| 18 | | `token` | `string` | yes | The verification code the user receives by email. | 19 | 20 | --- 21 | 22 | ## Example Request 23 | ```http 24 | GET /identity/auth/verify HTTP/1.1 25 | token=czZCaGRSa3F0MzpnWDFmQmF0M2JW 26 | ``` 27 | 28 | --- 29 | 30 | ## Responses 31 | 32 | ### Success Response 33 | #### HTTP Status Code: `200 OK` 34 | >*Note:* There is no response body for successful validation. 35 | 36 | --- 37 | 38 | ## Error Responses 39 | 40 | ### 1. Missing Required Parameter 41 | #### HTTP Status Code: `400 Bad Request` 42 | #### Response Body: 43 | ```json 44 | { 45 | "error": "invalid_request", 46 | "error_description": "failed to validate user account", 47 | "error_details": "missing one or more required parameters in the request" 48 | } 49 | ``` 50 | 51 | ### 2. Invalid Verification Code 52 | #### HTTP Status Code: `401 Unauthorized` 53 | #### Response Body: 54 | ```json 55 | { 56 | "error": "unauthorized", 57 | "error_description": "failed to validate user account", 58 | "error_details": "the verification code is either expired or does not exist" 59 | } 60 | ``` 61 | 62 | ### 3. Verification Code does not match with the User 63 | #### HTTP Status Code: `401 Unauthorized` 64 | #### Response Body: 65 | ```json 66 | { 67 | "error": "unauthorized", 68 | "error_description": "failed to validate user account", 69 | "error_details": "the verification code is invalid" 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vigiloauth/vigilo/v2 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.2.1 7 | github.com/joho/godotenv v1.5.1 8 | github.com/stretchr/testify v1.10.0 9 | golang.org/x/crypto v0.35.0 10 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 11 | ) 12 | 13 | require gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/go-chi/cors v1.2.1 18 | github.com/golang-jwt/jwt v3.2.2+incompatible 19 | github.com/google/uuid v1.6.0 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 4 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 5 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 6 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 7 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 8 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 12 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 18 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 19 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 20 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= 24 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 2 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 3 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 4 | github.com/vigiloauth/vigilo v0.0.0/go.mod h1:sKUpLR3KmsbEqNb0rJrOWvdNSCWpYAsEXkI9crTjRuw= 5 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 6 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 7 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 8 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 9 | golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= 10 | golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= 11 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 12 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 13 | -------------------------------------------------------------------------------- /idp/config/audit_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | // AuditLogConfig holds configuration settings for audit logging, 6 | // such as how long to retain audit logs. 7 | type AuditLogConfig struct { 8 | retentionPeriod time.Duration 9 | } 10 | 11 | // AuditLogConfigOptions defines a function signature for modifying AuditLogConfig. 12 | type AuditLogConfigOptions func(*AuditLogConfig) 13 | 14 | // Default retention period for audit logs: 90 days. 15 | const defaultRetentionPeriod time.Duration = 90 * 24 * time.Hour 16 | 17 | // NewAuditLogConfig creates a new AuditLogConfig instance, 18 | // applying any provided options to override the default values. 19 | func NewAuditLogConfig(opts ...AuditLogConfigOptions) *AuditLogConfig { 20 | cfg := &AuditLogConfig{retentionPeriod: defaultRetentionPeriod} 21 | for _, opt := range opts { 22 | opt(cfg) 23 | } 24 | return cfg 25 | } 26 | 27 | // WithRetentionPeriod returns an option that sets a custom retention period 28 | // for audit logs. Use this with NewAuditLogConfig to override the default. 29 | func WithRetentionPeriod(retentionPeriod time.Duration) AuditLogConfigOptions { 30 | return func(alc *AuditLogConfig) { 31 | alc.retentionPeriod = retentionPeriod 32 | } 33 | } 34 | 35 | // RetentionPeriod returns the configured audit log retention period. 36 | func (alc *AuditLogConfig) RetentionPeriod() time.Duration { 37 | return alc.retentionPeriod 38 | } 39 | -------------------------------------------------------------------------------- /internal/background/audit_jobs.go: -------------------------------------------------------------------------------- 1 | package background 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/vigiloauth/vigilo/v2/idp/config" 8 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/audit" 9 | ) 10 | 11 | type AuditJobs struct { 12 | auditLogger domain.AuditLogger 13 | retentionPeriod time.Duration 14 | purgeInterval time.Duration 15 | logger *config.Logger 16 | module string 17 | } 18 | 19 | func NewAuditJobs(auditLogger domain.AuditLogger, retentionPeriod, purgeInterval time.Duration) *AuditJobs { 20 | return &AuditJobs{ 21 | auditLogger: auditLogger, 22 | retentionPeriod: retentionPeriod, 23 | purgeInterval: purgeInterval, 24 | logger: config.GetServerConfig().Logger(), 25 | module: "Audit Jobs", 26 | } 27 | } 28 | 29 | func (a *AuditJobs) PurgeLogs(ctx context.Context) { 30 | a.logger.Info(a.module, "", "[PurgeLogs]: Starting process of removing old audit logs") 31 | ticker := time.NewTicker(a.purgeInterval) 32 | defer ticker.Stop() 33 | 34 | for { 35 | select { 36 | case <-ticker.C: 37 | cutoff := time.Now().Add(-a.retentionPeriod) 38 | if err := a.auditLogger.DeleteOldEvents(ctx, cutoff); err != nil { 39 | a.logger.Error(a.module, "", "[PurgeLogs]: There was an error deleting old audit logs: %v", err) 40 | continue //nolint 41 | } 42 | case <-ctx.Done(): 43 | a.logger.Info(a.module, "", "[PurgeLogs]: Stopping the process of deleting old audit logs") 44 | return 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/background/audit_jobs_test.go: -------------------------------------------------------------------------------- 1 | package background 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | mocks "github.com/vigiloauth/vigilo/v2/internal/mocks/audit" 11 | ) 12 | 13 | func TestAuditJobs_PurgeEvents(t *testing.T) { 14 | var mu sync.Mutex 15 | deleteCalls := 0 16 | 17 | auditLogger := &mocks.MockAuditLogger{ 18 | DeleteOldEventsFunc: func(cts context.Context, olderThan time.Time) error { 19 | mu.Lock() 20 | defer mu.Unlock() 21 | deleteCalls++ 22 | return nil 23 | }, 24 | } 25 | 26 | interval := 50 * time.Millisecond 27 | jobs := NewAuditJobs(auditLogger, interval, interval) 28 | ctx, cancel := context.WithTimeout(context.TODO(), 500*time.Millisecond) 29 | defer cancel() 30 | 31 | var wg sync.WaitGroup 32 | wg.Add(1) 33 | 34 | go func() { 35 | defer wg.Done() 36 | jobs.PurgeLogs(ctx) 37 | }() 38 | <-ctx.Done() 39 | wg.Wait() 40 | 41 | assert.GreaterOrEqual(t, deleteCalls, 1, "Should have called DeleteOldEvents at least once") 42 | } 43 | -------------------------------------------------------------------------------- /internal/background/scheduler.go: -------------------------------------------------------------------------------- 1 | package background 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/vigiloauth/vigilo/v2/idp/config" 8 | ) 9 | 10 | type JobFunc func(ctx context.Context) 11 | 12 | type Scheduler struct { 13 | mu sync.RWMutex 14 | jobs []JobFunc 15 | wg sync.WaitGroup 16 | stopCh chan struct{} 17 | logger *config.Logger 18 | module string 19 | } 20 | 21 | func NewScheduler() *Scheduler { 22 | return &Scheduler{ 23 | logger: config.GetServerConfig().Logger(), 24 | module: "Scheduler", 25 | stopCh: make(chan struct{}), 26 | } 27 | } 28 | 29 | func (s *Scheduler) RegisterJob(jobName string, job JobFunc) { 30 | s.mu.Lock() 31 | defer s.mu.Unlock() 32 | s.jobs = append(s.jobs, job) 33 | s.logger.Info(s.module, "", "[RegisterJob]: Registered job [%s]. Total jobs: %d", jobName, len(s.jobs)) 34 | } 35 | 36 | func (s *Scheduler) StartJobs(ctx context.Context) { 37 | s.logger.Info(s.module, "", "[StartJobs]: Starting %d background jobs...", len(s.jobs)) 38 | s.mu.RLock() 39 | defer s.mu.RUnlock() 40 | 41 | for i, job := range s.jobs { 42 | s.wg.Add(1) 43 | go func(i int, j JobFunc) { 44 | defer s.wg.Done() 45 | s.logger.Info(s.module, "", "[StartJobs]: Starting job #%d", i+1) 46 | j(ctx) 47 | }(i, job) 48 | } 49 | 50 | s.wg.Wait() 51 | s.logger.Info(s.module, "", "[StartJobs]: All background jobs completed.") 52 | } 53 | 54 | func (s *Scheduler) Stop() { 55 | close(s.stopCh) 56 | } 57 | 58 | func (s *Scheduler) Wait() { 59 | s.mu.Lock() 60 | defer s.mu.Unlock() 61 | s.logger.Info(s.module, "", "Waiting for all background jobs to finish...") 62 | s.wg.Wait() 63 | s.logger.Info(s.module, "", "All background jobs have finished.") 64 | } 65 | 66 | func (s *Scheduler) GetJobs() []JobFunc { 67 | s.mu.RLock() 68 | defer s.mu.RUnlock() 69 | return s.jobs 70 | } 71 | -------------------------------------------------------------------------------- /internal/background/token_jobs.go: -------------------------------------------------------------------------------- 1 | package background 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/vigiloauth/vigilo/v2/idp/config" 8 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/token" 9 | ) 10 | 11 | type TokenJobs struct { 12 | tokenService domain.TokenManager 13 | interval time.Duration 14 | logger *config.Logger 15 | module string 16 | } 17 | 18 | func NewTokenJobs(tokenService domain.TokenManager, interval time.Duration) *TokenJobs { 19 | return &TokenJobs{ 20 | tokenService: tokenService, 21 | interval: interval, 22 | logger: config.GetServerConfig().Logger(), 23 | module: "Token Jobs", 24 | } 25 | } 26 | 27 | func (t *TokenJobs) DeleteExpiredTokens(ctx context.Context) { 28 | t.logger.Info(t.module, "", "[DeleteExpiredTokens]: Starting process of deleting expired tokens") 29 | ticker := time.NewTicker(t.interval) 30 | defer ticker.Stop() 31 | 32 | for { 33 | select { 34 | case <-ticker.C: 35 | if err := t.tokenService.DeleteExpiredTokens(ctx); err != nil { 36 | t.logger.Error(t.module, "", "[DeleteExpiredTokens]: An error occurred deleting expired tokens: %v", err) 37 | continue //nolint 38 | } 39 | case <-ctx.Done(): 40 | t.logger.Info(t.module, "", "[DeleteExpiredTokens]: Stopping process of deleting expired tokens") 41 | return 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/background/token_jobs_test.go: -------------------------------------------------------------------------------- 1 | package background 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | mocks "github.com/vigiloauth/vigilo/v2/internal/mocks/token" 11 | ) 12 | 13 | func TestTokenJobs_DeleteExpiredTokens(t *testing.T) { 14 | var mu sync.Mutex 15 | deleteCalls := 0 16 | 17 | tokenService := &mocks.MockTokenManager{ 18 | DeleteExpiredTokensFunc: func(ctx context.Context) error { 19 | mu.Lock() 20 | defer mu.Unlock() 21 | deleteCalls++ 22 | return nil 23 | }, 24 | } 25 | 26 | interval := 50 * time.Millisecond 27 | jobs := NewTokenJobs(tokenService, interval) 28 | ctx, cancel := context.WithTimeout(context.TODO(), 500*time.Millisecond) 29 | defer cancel() 30 | 31 | var wg sync.WaitGroup 32 | wg.Add(1) 33 | 34 | go func() { 35 | defer wg.Done() 36 | jobs.DeleteExpiredTokens(ctx) 37 | }() 38 | <-ctx.Done() 39 | wg.Wait() 40 | 41 | assert.GreaterOrEqual(t, deleteCalls, 1, "Should have called DeleteExpiredTokens at least once") 42 | } 43 | -------------------------------------------------------------------------------- /internal/background/user_jobs.go: -------------------------------------------------------------------------------- 1 | package background 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/vigiloauth/vigilo/v2/idp/config" 8 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/user" 9 | ) 10 | 11 | type UserJobs struct { 12 | userService domain.UserManager 13 | interval time.Duration 14 | logger *config.Logger 15 | module string 16 | } 17 | 18 | func NewUserJobs(userService domain.UserManager, interval time.Duration) *UserJobs { 19 | return &UserJobs{ 20 | userService: userService, 21 | interval: interval, 22 | logger: config.GetServerConfig().Logger(), 23 | module: "User Jobs", 24 | } 25 | } 26 | 27 | func (u *UserJobs) DeleteUnverifiedUsers(ctx context.Context) { 28 | u.logger.Info(u.module, "", "[DeleteUnverifiedUsers]: Starting Process of deleting unverified users that were created over a week ago") 29 | ticker := time.NewTicker(u.interval) 30 | defer ticker.Stop() 31 | 32 | for { 33 | select { 34 | case <-ticker.C: 35 | if err := u.userService.DeleteUnverifiedUsers(ctx); err != nil { 36 | u.logger.Error(u.module, "", "[DeleteUnverifiedUsers]: Failed to delete unverified users: %v", err) 37 | continue //nolint:nlreturn 38 | } 39 | 40 | case <-ctx.Done(): 41 | u.logger.Info(u.module, "", "[DeleteUnverifiedUsers]: Stopping process of deleting unverified users") 42 | return //nolint:nlreturn 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/background/user_jobs_test.go: -------------------------------------------------------------------------------- 1 | package background 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | mocks "github.com/vigiloauth/vigilo/v2/internal/mocks/user" 11 | ) 12 | 13 | func TestUserJobs_DeleteUnverifiedUsers(t *testing.T) { 14 | var mu sync.Mutex 15 | deleteCalls := 0 16 | 17 | userService := &mocks.MockUserManager{ 18 | DeleteUnverifiedUsersFunc: func(ctx context.Context) error { 19 | mu.Lock() 20 | defer mu.Unlock() 21 | deleteCalls++ 22 | return nil 23 | }, 24 | } 25 | 26 | interval := 50 * time.Millisecond 27 | jobs := NewUserJobs(userService, interval) 28 | ctx, cancel := context.WithTimeout(context.TODO(), 500*time.Millisecond) 29 | defer cancel() 30 | 31 | var wg sync.WaitGroup 32 | wg.Add(1) 33 | 34 | go func() { 35 | defer wg.Done() 36 | jobs.DeleteUnverifiedUsers(ctx) 37 | }() 38 | <-ctx.Done() 39 | wg.Wait() 40 | 41 | assert.GreaterOrEqual(t, deleteCalls, 1, "Should have called DeleteUnverifiedUsers at least once") 42 | } 43 | -------------------------------------------------------------------------------- /internal/constants/application_types.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Predefined client application types. 4 | const ( 5 | WebApplicationType string = "web" 6 | NativeApplicationType string = "native" 7 | ) 8 | 9 | var ValidApplicationTypes = map[string]bool{ 10 | WebApplicationType: true, 11 | NativeApplicationType: true, 12 | } 13 | -------------------------------------------------------------------------------- /internal/constants/audit.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Audit Event Field Keys define the keys used for logging and tracking 4 | // details of audit events in the application. 5 | const ( 6 | ActionDetails string = "action" // Key for storing the action performed (e.g., "create", "update"). 7 | MethodDetails string = "method" // Key for storing the method used (e.g., "POST", "GET"). 8 | ) 9 | -------------------------------------------------------------------------------- /internal/constants/claims.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | SubClaim string = "sub" 5 | NameClaim string = "name" 6 | GivenNameClaim string = "given_name" 7 | FamilyNameClaim string = "family_name" 8 | MiddleNameClaim string = "middle_name" 9 | NicknameClaim string = "nickname" 10 | PreferredUsernameClaim string = "preferred_username" 11 | ProfileClaim string = "profile" 12 | PictureClaim string = "picture" 13 | WebsiteClaim string = "website" 14 | GenderClaim string = "gender" 15 | BirthdateClaim string = "birthdate" 16 | ZoneinfoClaim string = "zoneinfo" 17 | LocaleClaim string = "locale" 18 | EmailClaim string = "email" 19 | EmailVerifiedClaim string = "email_verified" 20 | PhoneNumberClaim string = "phone_number" 21 | PhoneNumberVerifiedClaim string = "phone_number_verified" 22 | UpdatedAtClaim string = "updated_at" 23 | AddressClaim string = "address" 24 | ) 25 | 26 | var SupportedClaims = map[string]bool{ 27 | SubClaim: true, 28 | NameClaim: true, 29 | GivenNameClaim: true, 30 | FamilyNameClaim: true, 31 | MiddleNameClaim: true, 32 | NicknameClaim: true, 33 | PreferredUsernameClaim: true, 34 | ProfileClaim: true, 35 | PictureClaim: true, 36 | WebsiteClaim: true, 37 | GenderClaim: true, 38 | BirthdateClaim: true, 39 | ZoneinfoClaim: true, 40 | LocaleClaim: true, 41 | EmailClaim: true, 42 | EmailVerifiedClaim: true, 43 | PhoneNumberClaim: true, 44 | PhoneNumberVerifiedClaim: true, 45 | UpdatedAtClaim: true, 46 | AddressClaim: true, 47 | } 48 | -------------------------------------------------------------------------------- /internal/constants/context.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // ContextKey defines a type for keys used to store and retrieve values in a context. 4 | // These keys are used throughout the application to pass and access request-specific data. 5 | type ContextKey string 6 | 7 | const ( 8 | ContextKeyIPAddress ContextKey = "ip_address" // Key for storing the client's IP address in the context 9 | ContextKeyRequestID ContextKey = "requestID" // Key for storing the unique request ID in the context 10 | ContextKeySessionID ContextKey = "session_id" // Key for storing the session ID in the context 11 | ContextKeyUserAgent ContextKey = "user_agent" // Key for storing the client's user agent in the context 12 | ContextKeyUserID ContextKey = "user_id" // Key for storing the user ID in the context 13 | ContextKeyTokenClaims ContextKey = "token_claims" // Key for storing token claims in the context 14 | ContextKeyAccessToken ContextKey = "access_token" // Key for storing the access token in the context 15 | ) 16 | -------------------------------------------------------------------------------- /internal/constants/displays.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | DisplayPage string = "page" 5 | DisplayPopup string = "popup" 6 | ) 7 | 8 | var ValidAuthenticationDisplays = map[string]bool{ 9 | DisplayPage: true, 10 | DisplayPopup: true, 11 | } 12 | -------------------------------------------------------------------------------- /internal/constants/email.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Email Content Labels 4 | const ( 5 | FromAddress string = "From" 6 | EmailSubject string = "Subject" 7 | Recipient string = "To" 8 | HTMLBody string = "text/html" 9 | VerifyEmailAddress string = "Verify Your Email Address" 10 | AccountDeletion string = "Your Account Has Been Deleted" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/constants/env.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Environment Variable Names 4 | const ( 5 | CryptoSecretKeyENV string = "CRYPTO_SECRET_KEY" 6 | SMTPFromAddressENV string = "SMTP_FROM_ADDRESS" 7 | SMTPPasswordENV string = "SMTP_PASSWORD" 8 | SMTPUsernameENV string = "SMTP_USERNAME" 9 | TokenIssuerENV string = "TOKEN_ISSUER" 10 | TokenPrivateKeyENV string = "TOKEN_PRIVATE_KEY" 11 | TokenPublicKeyENV string = "TOKEN_PUBLIC_KEY" 12 | ReactBuildPathENV string = "REACT_BUILD_PATH" 13 | VigiloServerModeENV string = "VIGILO_SERVER_MODE" 14 | 15 | EnvFilePath string = "../../.env" 16 | TestEnvFilePath string = "../../../.env.test" 17 | ) 18 | -------------------------------------------------------------------------------- /internal/constants/grant_types.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Grant types define the methods by which a client can obtain an access token 4 | // in the OAuth 2.0 and OpenID Connect protocols. These constants represent 5 | // the supported grant types in the application. 6 | const ( 7 | AuthorizationCodeGrantType string = "authorization_code" // Standard OAuth 2.0 Authorization Code Grant 8 | ClientCredentialsGrantType string = "client_credentials" // Client Credentials Grant 9 | DeviceCodeGrantType string = "urn:ietf:params:oauth:grant-type:device_code" // Device Code Grant 10 | RefreshTokenGrantType string = "refresh_token" // Refresh Token Grant 11 | ImplicitGrantType string = "implicit" // Implicit Flow (deprecated in OAuth 2.1) 12 | PasswordGrantType string = "password" // Resource Owner Password Credentials Grant (deprecated) 13 | ) 14 | 15 | // SupportedGrantTypes is a map of grant types supported by the application. 16 | // The key is the grant type, and the value indicates whether it is supported. 17 | var SupportedGrantTypes = map[string]bool{ 18 | AuthorizationCodeGrantType: true, 19 | ClientCredentialsGrantType: true, 20 | DeviceCodeGrantType: true, 21 | RefreshTokenGrantType: true, 22 | ImplicitGrantType: true, 23 | PasswordGrantType: true, 24 | } 25 | -------------------------------------------------------------------------------- /internal/constants/ids.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // ID Prefix Constants define prefixes used for generating unique identifiers 4 | // for various entities in the application. 5 | const ( 6 | AuditEventIDPrefix string = "audit-" // Prefix for audit event IDs 7 | SessionIDPrefix string = "sess-" // Prefix for session IDs 8 | RequestIDPrefix string = "req-" // Prefix for request IDs 9 | UserRoleIDPrefix string = "user-" // Prefix for user role IDs 10 | AdminRoleIDPrefix string = "admin-" // Prefix for admin role IDs 11 | ClientIDPrefix string = "client-" // Prefix for client IDs 12 | ) 13 | -------------------------------------------------------------------------------- /internal/constants/oidc.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // OIDC Constants define values used in OpenID Connect (OIDC) for token signing, 4 | // encryption, subject types, and client authentication methods. 5 | const ( 6 | SubjectTypePublic string = "public" // Public subject type for OIDC (e.g., non-pairwise identifiers) 7 | SubjectTypePairwise string = "pairwise" 8 | IDTokenSigningAlgorithmRS256 string = "RS256" // Signing algorithm for ID tokens (RSA with SHA-256) 9 | IDTokenEncryptionAlgorithmRSA string = "RSA-OAEP" // Encryption algorithm for ID tokens (RSA-OAEP) 10 | AuthMethodClientSecretPost string = "client_secret_post" // Client authentication using client_id and client_secret in the request body 11 | AuthMethodClientSecretBasic string = "client_secret_basic" // Client authentication using client_id and client_secret in the Authorization header 12 | AuthMethodNone string = "none" // No client authentication (used for public clients, e.g., with PKCE) 13 | ) 14 | -------------------------------------------------------------------------------- /internal/constants/prompts.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | PromptLogin string = "login" 5 | PromptNone string = "none" 6 | PromptConsent string = "consent" 7 | ) 8 | 9 | var ValidPrompts = map[string]bool{ 10 | PromptLogin: true, 11 | PromptNone: true, 12 | PromptConsent: true, 13 | } 14 | -------------------------------------------------------------------------------- /internal/constants/response_types.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Response Types define the types of responses that can be returned 4 | // in OAuth 2.0 and OpenID Connect flows. 5 | const ( 6 | CodeResponseType string = "code" // Authorization Code response type 7 | TokenResponseType string = "token" // Implicit Flow token response type 8 | IDTokenResponseType string = "id_token" // ID Token response type for OpenID Connect 9 | HybridResponseType string = "token id_token" 10 | ) 11 | 12 | // SupportedResponseTypes is a map of response types supported by the application. 13 | // The key is the response type, and the value indicates whether it is supported. 14 | var SupportedResponseTypes = map[string]bool{ 15 | CodeResponseType: true, 16 | TokenResponseType: true, 17 | IDTokenResponseType: true, 18 | HybridResponseType: true, 19 | } 20 | -------------------------------------------------------------------------------- /internal/constants/roles.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Predefined user roles define the roles that can be assigned to users 4 | // in the application. These roles determine the level of access and permissions. 5 | const ( 6 | UserRole string = "USER" // Standard user role with limited access 7 | AdminRole string = "ADMIN" // Administrator role with elevated privileges 8 | ) 9 | 10 | // ValidRoles is a map of roles supported by the application. 11 | // The key is the role, and the value indicates whether it is valid. 12 | var ValidRoles = map[string]bool{ 13 | UserRole: true, 14 | AdminRole: true, 15 | } 16 | -------------------------------------------------------------------------------- /internal/constants/timeouts.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "time" 4 | 5 | const ( 6 | ThreeSecondTimeout time.Duration = 3 * time.Second 7 | FiveSecondTimeout time.Duration = 5 * time.Second 8 | ThirtySecondTimeout time.Duration = 30 * time.Second 9 | TenSecondTimeout time.Duration = 10 * time.Second 10 | ) 11 | -------------------------------------------------------------------------------- /internal/container/lazy_init.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import "sync" 4 | 5 | // LazyInit is a generic type that provides lazy initialization for a value of type T. 6 | // It ensures that the initialization function (initFunc) is executed only once, 7 | // regardless of how many times the value is accessed. 8 | // 9 | // Fields: 10 | // - once: A sync.Once instance used to guarantee that the initialization function 11 | // is executed only once. 12 | // - value: The lazily initialized value of type T. 13 | // - initFunc: A function that initializes and returns the value of type T. 14 | // 15 | // Usage: 16 | // LazyInit can be used to defer the computation or initialization of a value 17 | // until it is actually needed, while ensuring thread-safe access. 18 | type LazyInit[T any] struct { 19 | once sync.Once 20 | value T 21 | initFunc func() T 22 | } 23 | 24 | // Get retrieves the value of the LazyInit instance, initializing it if necessary. 25 | // The initialization is performed only once using the provided initFunc. 26 | // Subsequent calls to Get will return the already initialized value. 27 | // 28 | // Returns: 29 | // - T: The initialized value of the LazyInit instance. 30 | func (l *LazyInit[T]) Get() T { 31 | l.once.Do(func() { 32 | l.value = l.initFunc() 33 | }) 34 | 35 | return l.value 36 | } 37 | -------------------------------------------------------------------------------- /internal/container/server_config_registry.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/vigiloauth/vigilo/v2/idp/config" 9 | ) 10 | 11 | type ServerConfigRegistry struct { 12 | tlsConfig *tls.Config 13 | httpServer *http.Server 14 | } 15 | 16 | func NewServerConfigRegistry(services *ServiceRegistry) *ServerConfigRegistry { 17 | sr := &ServerConfigRegistry{} 18 | sr.initServerConfigurations() 19 | return sr 20 | } 21 | 22 | func (sr *ServerConfigRegistry) HTTPServer() *http.Server { 23 | return sr.httpServer 24 | } 25 | 26 | func (sr *ServerConfigRegistry) initServerConfigurations() { 27 | sr.initTLS() 28 | sr.initHTTPServer() 29 | } 30 | 31 | func (sr *ServerConfigRegistry) initTLS() { 32 | sr.tlsConfig = &tls.Config{ 33 | MinVersion: tls.VersionTLS12, 34 | CipherSuites: []uint16{ 35 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 36 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 37 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 38 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 39 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 40 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 41 | }, 42 | } 43 | } 44 | 45 | func (sr *ServerConfigRegistry) initHTTPServer() { 46 | sr.httpServer = &http.Server{ 47 | Addr: fmt.Sprintf(":%s", config.GetServerConfig().Port()), 48 | ReadTimeout: config.GetServerConfig().ReadTimeout(), 49 | WriteTimeout: config.GetServerConfig().WriteTimeout(), 50 | TLSConfig: sr.tlsConfig, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/domain/audit/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // AuditRepository defines the interface for storing and retrieving audit events. 9 | type AuditRepository interface { 10 | // StoreAuditEvent stores an audit event in the repository. 11 | // 12 | // Parameters: 13 | // - ctx Context: The context for managing timeouts and cancellations. 14 | // - event *AuditEvent: The audit event to be stored. 15 | // 16 | // Returns: 17 | // - error: An error if storing the event fails, otherwise nil. 18 | StoreAuditEvent(ctx context.Context, event *AuditEvent) error 19 | 20 | // GetAuditEvents retrieves audit events that match the provided filters and time range. 21 | // 22 | // Parameters: 23 | // - ctx Context: The context for managing timeouts and cancellations. 24 | // - filters map[string]any: A map of filter keys and values to apply. 25 | // - from time.Time: The start time of the time range to filter events. 26 | // - to time.Time: The end time of the time range to filter events. 27 | // - limit int: The maximum number of events to return. 28 | // - offset int: The number of events to skip (for pagination). 29 | // 30 | // Returns: 31 | // - []*AuditEvent: A slice of matching audit events. 32 | // - error: An error if the retrieval fails, otherwise nil. 33 | GetAuditEvents(ctx context.Context, filters map[string]any, from time.Time, to time.Time, limit, offset int) ([]*AuditEvent, error) 34 | 35 | // DeleteEvent deletes an event using the given event ID. 36 | // 37 | // Parameters: 38 | // - ctx Context: The context for managing timeouts and cancellations. 39 | // - eventID string: The ID of the event to delete. 40 | // 41 | // Returns: 42 | // - error: An error if deletion fails, otherwise nil. 43 | DeleteEvent(ctx context.Context, eventID string) error 44 | } 45 | -------------------------------------------------------------------------------- /internal/domain/audit/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type AuditLogger interface { 9 | // StoreEvent saves an AuditEvent to the repository. 10 | // If an error occurs storing the audit event, no error will be returned so that the flow is not disrupted. 11 | // 12 | // Parameters: 13 | // - ctx Context: The context for managing timeouts, cancellations, and for storing/retrieving event metadata. 14 | // - eventType EventType: The type of event to store. 15 | // - success bool: True if the event was successful, otherwise false. 16 | // - action ActionType: The action that is to be audited. 17 | // - method MethodType: The method used (password, email, etc). 18 | // - err error: The error if applicable, otherwise nil. 19 | StoreEvent(ctx context.Context, eventType EventType, success bool, action ActionType, method MethodType, err error) 20 | 21 | // DeleteOldEvents deletes audit events older than the specified timestamp. 22 | // 23 | // Parameters: 24 | // - ctx Context: The context for managing timeouts and cancellations. 25 | // - olderThan time.Time: Events older than this timestamp will be deleted. 26 | // 27 | // Returns: 28 | // - error: An error if deletion fails, otherwise nil. 29 | DeleteOldEvents(ctx context.Context, olderThan time.Time) error 30 | 31 | // GetAuditEvents retrieves audit events that match the provided filters and time range. 32 | // 33 | // Parameters: 34 | // - ctx Context: The context for managing timeouts and cancellations. 35 | // - filters map[string]any: A map of filter keys and values to apply. 36 | // - from string: The start time of the time range to filter events (must be RFC3339 Format). 37 | // - to string: The end time of the time range to filter events (must be RFC3339 Format). 38 | // - limit int: The maximum number of events to return. 39 | // - offset int: The number of events to skip (for pagination). 40 | // 41 | // Returns: 42 | // - []*AuditEvent: A slice of matching audit events. 43 | // - error: An error if the retrieval fails, otherwise nil. 44 | GetAuditEvents(ctx context.Context, filters map[string]any, fromStr string, toStr string, limit, offset int) ([]*AuditEvent, error) 45 | } 46 | -------------------------------------------------------------------------------- /internal/domain/authorization/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | authz "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 7 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 8 | users "github.com/vigiloauth/vigilo/v2/internal/domain/user" 9 | ) 10 | 11 | // AuthorizationService defines the interface for handling client authorization requests. 12 | type AuthorizationService interface { 13 | 14 | // AuthorizeTokenExchange validates the token exchange request for an OAuth 2.0 authorization code grant. 15 | // 16 | // Parameters: 17 | // - ctx Context: The context for managing timeouts and cancellations. 18 | // - tokenRequest token.TokenRequest: The token exchange request containing client and authorization code details. 19 | // 20 | // Returns: 21 | // - *AuthorizationCodeData: The authorization code data if authorization is successful. 22 | // - error: An error if the token exchange request is invalid or fails authorization checks. 23 | AuthorizeTokenExchange(ctx context.Context, tokenRequest *token.TokenRequest) (*authz.AuthorizationCodeData, error) 24 | 25 | // AuthorizeUserInfoRequest validates whether the provided access token claims grant sufficient 26 | // permission to access the /userinfo endpoint. 27 | // 28 | // This method is responsible for performing authorization checks and retrieving the user only. It does not validate the token itself (assumes 29 | // the token has already been validated by the time this method is called). 30 | // 31 | // Parameters: 32 | // - ctx context.Context: The context for managing timeouts and cancellations. 33 | // - claims *TokenClaims: The token claims extracted from the a valid access token. These claims should include the 34 | // 'scope' field, which will be used to verify whether the client is authorized for the request. 35 | // 36 | // Returns: 37 | // - *User: The retrieved user if authorization succeeds, otherwise nil. 38 | // - error: An error if authorization fails, otherwise nil. 39 | AuthorizeUserInfoRequest(ctx context.Context, claims *token.TokenClaims) (*users.User, error) 40 | 41 | // UpdateAuthorizationCode updates the authorization code data in the database. 42 | // 43 | // Parameters: 44 | // - ctx context.Context: The context for managing timeouts and cancellations. 45 | // - authData *AuthorizationCodeData: The authorization code data to update. 46 | // 47 | // Returns: 48 | // - error: An error if the update fails, otherwise nil. 49 | UpdateAuthorizationCode(ctx context.Context, authData *authz.AuthorizationCodeData) error 50 | } 51 | -------------------------------------------------------------------------------- /internal/domain/authzcode/creator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | type AuthorizationCodeCreator interface { 10 | // GenerateAuthorizationCode creates a new authorization code and stores it with associated data. 11 | // 12 | // Parameters: 13 | // - ctx Context: The context for managing timeouts and cancellations. 14 | // - req *ClientAuthorizationRequest: The request containing the metadata to generate an authorization code. 15 | // 16 | // Returns: 17 | // - string: The generated authorization code. 18 | // - error: An error if code generation fails. 19 | GenerateAuthorizationCode(ctx context.Context, req *client.ClientAuthorizationRequest) (string, error) 20 | } 21 | -------------------------------------------------------------------------------- /internal/domain/authzcode/issuer.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | type AuthorizationCodeIssuer interface { 10 | // IssueAuthorizationCode generates an authorization code for the given client request. 11 | // 12 | // Parameters: 13 | // - ctx Context: The context for managing timeouts and cancellations. 14 | // - req *ClientAuthorizationRequest: The request containing the metadata to generate an authorization code. 15 | // 16 | // Returns: 17 | // - string: The generated authorization code. 18 | // - error: An error if code generation fails. 19 | IssueAuthorizationCode(ctx context.Context, req *client.ClientAuthorizationRequest) (string, error) 20 | } 21 | -------------------------------------------------------------------------------- /internal/domain/authzcode/model.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/claims" 8 | "github.com/vigiloauth/vigilo/v2/internal/types" 9 | ) 10 | 11 | // AuthorizationCodeData represents the data associated with an authorization code. 12 | type AuthorizationCodeData struct { 13 | UserID string 14 | ClientID string 15 | RedirectURI string 16 | Scope types.Scope 17 | Code string 18 | CodeChallenge string 19 | CodeChallengeMethod types.CodeChallengeMethod 20 | Nonce string 21 | AccessTokenHash string 22 | Used bool 23 | CreatedAt time.Time 24 | UserAuthenticationTime time.Time 25 | Request *http.Request 26 | ClaimsRequest *domain.ClaimsRequest 27 | ACRValues string 28 | } 29 | -------------------------------------------------------------------------------- /internal/domain/authzcode/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type AuthorizationCodeRepository interface { 9 | // StoreAuthorizationCode persists an authorization code with its associated data. 10 | // 11 | // Parameters: 12 | // - ctx Context: The context for managing timeouts and cancellations. 13 | // - code string: The authorization code. 14 | // - data *AuthorizationCodeData: The data associated with the code. 15 | // - expiresAt time.Time: When the code expires. 16 | // 17 | // Returns: 18 | // - error: An error if storing fails, nil otherwise. 19 | StoreAuthorizationCode(ctx context.Context, code string, data *AuthorizationCodeData, expiresAt time.Time) error 20 | 21 | // GetAuthorizationCode retrieves the data associated with an authorization code. 22 | // 23 | // Parameters: 24 | // - ctx Context: The context for managing timeouts and cancellations. 25 | // - code string: The authorization code to look up. 26 | // 27 | // Returns: 28 | // - *AuthorizationCodeData: The associated data if found. 29 | // - error: An error if retrieval fails. 30 | GetAuthorizationCode(ctx context.Context, code string) (*AuthorizationCodeData, error) 31 | 32 | // DeleteAuthorizationCode deletes an authorization code after use. 33 | // 34 | // Parameters: 35 | // - ctx Context: The context for managing timeouts and cancellations. 36 | // - code string: The authorization code to remove. 37 | // 38 | // Returns: 39 | // - error: An error if removal fails, nil otherwise. 40 | DeleteAuthorizationCode(ctx context.Context, code string) error 41 | 42 | // UpdateAuthorizationCode updates existing authorization code data. 43 | // 44 | // Parameters: 45 | // - ctx Context: The context for managing timeouts and cancellations. 46 | // - code string: The authorization code to update. 47 | // - authData *AuthorizationCodeData: The update authorization code data. 48 | // 49 | // Returns: 50 | // - error: An error if update fails, nil otherwise. 51 | UpdateAuthorizationCode(ctx context.Context, code string, authData *AuthorizationCodeData) error 52 | } 53 | -------------------------------------------------------------------------------- /internal/domain/authzcode/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type AuthorizationCodeManager interface { 8 | // RevokeAuthorizationCode explicitly invalidates a code. 9 | // 10 | // Parameters: 11 | // - ctx Context: The context for managing timeouts and cancellations. 12 | // - code string: The authorization code to revoke. 13 | // 14 | // Returns: 15 | // - error: An error if revocation fails. 16 | RevokeAuthorizationCode(ctx context.Context, code string) error 17 | 18 | // GetAuthorizationCode retrieves the authorization code data for a given code. 19 | // 20 | // Parameters: 21 | // - ctx Context: The context for managing timeouts and cancellations. 22 | // - code string: The authorization code to retrieve. 23 | // 24 | // Returns: 25 | // - *AuthorizationCodeData: The authorization code data if found, or nil if no matching code exists. 26 | GetAuthorizationCode(ctx context.Context, code string) (*AuthorizationCodeData, error) 27 | 28 | // UpdateAuthorizationCode updates the provided authorization code data in the repository. 29 | // 30 | // Parameters: 31 | // - ctx Context: The context for managing timeouts and cancellations. 32 | // - authData (*authz.AuthorizationCodeData): The authorization code data to be updated. 33 | // 34 | // Returns: 35 | // - error: An error if updated the authorization code fails, or nil if the operation succeeds. 36 | UpdateAuthorizationCode(ctx context.Context, authData *AuthorizationCodeData) error 37 | } 38 | -------------------------------------------------------------------------------- /internal/domain/authzcode/validation.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "github.com/vigiloauth/vigilo/v2/internal/errors" 4 | 5 | func (c *AuthorizationCodeData) ValidateFields(clientID, redirectURI string) error { 6 | if c.Used { 7 | return errors.New(errors.ErrCodeInvalidGrant, "authorization code has already been used") 8 | } else if c.ClientID != clientID { 9 | return errors.New(errors.ErrCodeInvalidGrant, "authorization code client ID and request client ID do no match") 10 | } else if c.RedirectURI != redirectURI { 11 | return errors.New(errors.ErrCodeInvalidGrant, "authorization code redirect URI and request redirect URI do no match") 12 | } 13 | 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/domain/authzcode/validator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | type AuthorizationCodeValidator interface { 10 | // ValidateRequest checks the validity of the client authorization request. 11 | // 12 | // Parameters: 13 | // - ctx Context: The context for managing timeouts and cancellations. 14 | // - req *ClientAuthorizationRequest: The request to validate. 15 | // 16 | // Returns: 17 | // - error: An error if the request is invalid. 18 | ValidateRequest(ctx context.Context, req *client.ClientAuthorizationRequest) error 19 | 20 | // ValidateAuthorizationCode checks if a code is valid and returns the associated data. 21 | // 22 | // Parameters: 23 | // - ctx Context: The context for managing timeouts and cancellations. 24 | // - code string: The authorization code to validate. 25 | // - clientID string: The client requesting validation. 26 | // - redirectURI string: The redirect URI to verify. 27 | // 28 | // Returns: 29 | // - error: An error if validation fails. 30 | ValidateAuthorizationCode(ctx context.Context, code, clientID, redirectURI string) error 31 | 32 | // ValidatePKCE validates the PKCE (Proof Key for Code Exchange) parameters during the token exchange process. 33 | // 34 | // This method checks if the provided code verifier matches the code challenge stored in the authorization code data. 35 | // It supports the "S256" (SHA-256) and "plain" code challenge methods. 36 | // 37 | // Parameters: 38 | // - ctx Context: The context for managing timeouts and cancellations. 39 | // - authzCodeData (*authz.AuthorizationCodeData): The authorization code data containing the code challenge and method. 40 | // - codeVerifier (string): The code verifier provided by the client during the token exchange. 41 | // 42 | // Returns: 43 | // - error: An error if the validation fails, including cases where the code verifier does not match the code challenge 44 | // or if the code challenge method is unsupported. Returns nil if validation succeeds. 45 | ValidatePKCE(ctx context.Context, authzCodeData *AuthorizationCodeData, codeVerifier string) error 46 | } 47 | -------------------------------------------------------------------------------- /internal/domain/claims/model.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | ) 7 | 8 | type ClaimsRequest struct { 9 | UserInfo *ClaimSet `json:"userinfo,omitempty"` 10 | } 11 | 12 | type ClaimSet map[string]*ClaimSpec 13 | 14 | type ClaimSpec struct { 15 | Essential bool `json:"essential,omitempty"` 16 | Value string `json:"value,omitempty"` 17 | } 18 | 19 | func ParseClaimsParameter(claimsParam string) (*ClaimsRequest, error) { 20 | decodedClaims, _ := url.QueryUnescape(claimsParam) 21 | var claimsRequest ClaimsRequest 22 | _ = json.Unmarshal([]byte(decodedClaims), &claimsRequest) 23 | return &claimsRequest, nil 24 | } 25 | 26 | func SerializeClaims(claims *ClaimsRequest) string { 27 | if claims != nil { 28 | claimsJSON, err := json.Marshal(claims) 29 | if err != nil { 30 | claimsJSON = nil 31 | } 32 | return string(claimsJSON) 33 | } 34 | 35 | return "" 36 | } 37 | -------------------------------------------------------------------------------- /internal/domain/client/authenticator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/vigiloauth/vigilo/v2/internal/types" 8 | ) 9 | 10 | // ClientAuthenticator defines an interface for authenticating HTTP client requests. 11 | type ClientAuthenticator interface { 12 | // AuthenticateRequest validates the incoming HTTP request to ensure the client has the required scope. 13 | // 14 | // Parameters: 15 | // - ctx context.Context: The context for managing timeouts and cancellations. 16 | // - r *http.Request: The HTTP request to authenticate. 17 | // - requiredScope types.Scope: The scope required to access the requested resource. 18 | // 19 | // Returns: 20 | // - error: An error if authentication fails or the required scope is not met. 21 | AuthenticateRequest(ctx context.Context, r *http.Request, requiredScope types.Scope) error 22 | 23 | // AuthenticateClient authenticates the client using provided credentials 24 | // and authorizes access by validating required grant types and scopes. 25 | // 26 | // Parameters: 27 | // - ctx Context: The context for managing timeouts and cancellations. 28 | // - req *ClientAuthenticationRequest: The request containing client credentials and required scopes. 29 | // 30 | // Returns: 31 | // - error: An error if authentication or authorization fails. 32 | AuthenticateClient(ctx context.Context, req *ClientAuthenticationRequest) error 33 | } 34 | -------------------------------------------------------------------------------- /internal/domain/client/authorization.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type ClientAuthorization interface { 6 | // Authorize handles the authorization logic for a client request. 7 | // 8 | // Parameters: 9 | // - ctx Context: The context for managing timeouts and cancellations. 10 | // - authorizationRequest *ClientAuthorizationRequest: The client authorization request. 11 | // 12 | // Returns: 13 | // - string: The redirect URL, or an empty string if authorization failed. 14 | // - error: An error message, if any. 15 | // 16 | // Errors: 17 | // - Returns an error message if the user is not authenticated, consent is denied, or authorization code generation fails. 18 | Authorize(ctx context.Context, request *ClientAuthorizationRequest) (string, error) 19 | } 20 | -------------------------------------------------------------------------------- /internal/domain/client/creator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type ClientCreator interface { 6 | Register(ctx context.Context, req *ClientRegistrationRequest) (*ClientRegistrationResponse, error) 7 | } 8 | -------------------------------------------------------------------------------- /internal/domain/client/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | // ClientRepository defines the interface for storing and managing Clients. 6 | type ClientRepository interface { 7 | // SaveClient adds a new client to the store if it does not already exist. 8 | // 9 | // Parameters: 10 | // - ctx Context: The context for managing timeouts and cancellations. 11 | // - client *Client: The client object to store. 12 | // 13 | // Returns: 14 | // - error: An error if the client already exists, nil otherwise. 15 | SaveClient(ctx context.Context, client *Client) error 16 | 17 | // GetClientByID retrieves a client by its ID. 18 | // 19 | // Parameters: 20 | // - ctx Context: The context for managing timeouts and cancellations. 21 | // - clientID string: The ID of the client to retrieve. 22 | // 23 | // Returns: 24 | // - *Client: The client object if found, nil otherwise. 25 | // - error: An error if retrieval fails. 26 | GetClientByID(ctx context.Context, clientID string) (*Client, error) 27 | 28 | // DeleteClient removes a client from the store by its ID. 29 | // 30 | // Parameters: 31 | // - ctx Context: The context for managing timeouts and cancellations. 32 | // - clientID string: The ID of the client to delete. 33 | // 34 | // Returns: 35 | // - error: Returns an error if deletion fails, otherwise false. 36 | DeleteClientByID(ctx context.Context, clientID string) error 37 | 38 | // UpdateClient updates an existing client in the store. 39 | // 40 | // Parameters: 41 | // - ctx Context: The context for managing timeouts and cancellations. 42 | // - client *Client: The updated client object. 43 | // 44 | // Returns: 45 | // - error: An error if the client does not exist, nil otherwise. 46 | UpdateClient(ctx context.Context, client *Client) error 47 | 48 | // IsExistingID checks to see if an ID already exists in the database. 49 | // 50 | // Parameters: 51 | // - ctx Context: The context for managing timeouts and cancellations. 52 | // - clientID string: The client ID to verify. 53 | // 54 | // Returns: 55 | // - bool: True if it exists, otherwise false. 56 | IsExistingID(ctx context.Context, clientID string) bool 57 | } 58 | -------------------------------------------------------------------------------- /internal/domain/client/validator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type ClientValidator interface { 8 | ValidateRegistrationRequest(ctx context.Context, req *ClientRegistrationRequest) error 9 | ValidateUpdateRequest(ctx context.Context, req *ClientUpdateRequest) error 10 | ValidateAuthorizationRequest(ctx context.Context, req *ClientAuthorizationRequest) error 11 | ValidateRedirectURI(ctx context.Context, redirectURI string, existingClient *Client) error 12 | ValidateClientAndRegistrationAccessToken(ctx context.Context, clientID string, registrationAccessToken string) error 13 | } 14 | -------------------------------------------------------------------------------- /internal/domain/cookies/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | type HTTPCookieService interface { 10 | // SetSessionCookie sets the session token in an HttpOnly cookie. 11 | // It also sets the cookie's expiration time and other attributes. 12 | // 13 | // Parameters: 14 | // - ctx Context: The context for managing timeouts and cancellations. 15 | // - w http.ResponseWriter: The HTTP response writer. 16 | // - sessionID string: The session ID to set in the cookie. 17 | // - expirationTime time.Duration: The expiration time for the cookie. 18 | SetSessionCookie(ctx context.Context, w http.ResponseWriter, sessionID string, expirationTime time.Duration) 19 | 20 | // ClearSessionCookie clears the session token cookie. 21 | // 22 | // Parameters: 23 | // - ctx Context: The context for managing timeouts and cancellations. 24 | // - w http.ResponseWriter: The HTTP response writer. 25 | ClearSessionCookie(ctx context.Context, w http.ResponseWriter) 26 | 27 | // GetSessionToken retrieves the session cookie from the request. 28 | // 29 | // Parameters: 30 | // - r *http.Request: The HTTP request containing the session. 31 | // 32 | // Returns: 33 | // - string: The session cookie if found, otherwise nil. 34 | // - error: An error if retrieving the cookie fails. 35 | GetSessionCookie(r *http.Request) (*http.Cookie, error) 36 | } 37 | -------------------------------------------------------------------------------- /internal/domain/email/mailer.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "gopkg.in/gomail.v2" 4 | 5 | // Mailer defines methods for interacting with an email service using gomail. 6 | // It includes functionality for dialing, sending emails, and creating email messages. 7 | type Mailer interface { 8 | // Dial establishes a connection to the email server with the provided host, port, 9 | // username, and password. It returns a `gomail.SendCloser` which can be used 10 | // to send emails, or an error if the connection fails. 11 | // 12 | // Parameters: 13 | // - host string: The email server's host. 14 | // - port string: The port number to connect to on the email server. 15 | // - username string: The username for authenticating to the email server. 16 | // - password string: The password for authenticating to the email server. 17 | // 18 | // Returns: 19 | // - gomail.SendCloser: A send closer to send emails. 20 | // - error: An error if the connection fails, or nil if successful. 21 | Dial(host string, port int, username string, password string) (gomail.SendCloser, error) 22 | 23 | // DialAndSend connects to the email server and immediately sends the provided email message(s). 24 | // It returns an error if the connection or sending process fails. 25 | // 26 | // Parameters: 27 | // - host string: The email server's host. 28 | // - port string: The port number to connect to on the email server. 29 | // - username string: The username for authenticating to the email server. 30 | // - password string: The password for authenticating to the email server. 31 | // - message string: A list of gomail messages to send. One or more messages can be provided. 32 | // 33 | // Returns: 34 | // - error: An error indicating the failure to send the email(s), or nil if successful. 35 | DialAndSend(host string, port int, username string, password string, message ...*gomail.Message) error 36 | 37 | // NewMessage creates a new gomail message using the provided email request, body, 38 | // subject, and sender address. This message can then be sent using the DialAndSend method. 39 | // 40 | // Parameters: 41 | // - request string: The request object containing email details. 42 | // - body string: The body content of the email. 43 | // - subject string: The subject of the email. 44 | // - fromAddress string: The sender's email address. 45 | // 46 | // Returns: 47 | // - *gomail.Message: A new gomail message containing the provided email details. 48 | NewMessage(request *EmailRequest, body string, subject string, fromAddress string) *gomail.Message 49 | } 50 | -------------------------------------------------------------------------------- /internal/domain/email/model.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/vigiloauth/vigilo/v2/internal/utils" 7 | ) 8 | 9 | type EmailRequest struct { 10 | Recipient string 11 | EmailType EmailType 12 | VerificationCode string 13 | VerificationToken string 14 | BaseURL string 15 | ID string 16 | Retries int 17 | } 18 | 19 | func NewEmailRequest(recipient, verificationCode, verificationToken string, emailType EmailType) *EmailRequest { 20 | return &EmailRequest{ 21 | Recipient: recipient, 22 | VerificationCode: verificationCode, 23 | VerificationToken: verificationCode, 24 | EmailType: emailType, 25 | ID: utils.GenerateUUID(), 26 | Retries: 0, 27 | } 28 | } 29 | 30 | type EmailType string 31 | 32 | const ( 33 | AccountVerification EmailType = "account_verification" 34 | AccountDeletion EmailType = "account_deletion" 35 | ) 36 | 37 | func (t EmailType) String() string { 38 | return string(t) 39 | } 40 | 41 | type EmailRetryQueue struct { 42 | mu sync.Mutex 43 | requests []*EmailRequest 44 | } 45 | 46 | func (q *EmailRetryQueue) Add(request *EmailRequest) { 47 | q.mu.Lock() 48 | defer q.mu.Unlock() 49 | q.requests = append(q.requests, request) 50 | } 51 | 52 | func (q *EmailRetryQueue) Remove() *EmailRequest { 53 | q.mu.Lock() 54 | defer q.mu.Unlock() 55 | 56 | if len(q.requests) == 0 { 57 | return nil 58 | } 59 | 60 | request := q.requests[0] 61 | q.requests = q.requests[1:] 62 | return request 63 | } 64 | 65 | func (q *EmailRetryQueue) IsEmpty() bool { 66 | q.mu.Lock() 67 | defer q.mu.Unlock() 68 | return len(q.requests) == 0 69 | } 70 | 71 | func (q *EmailRetryQueue) Size() int { 72 | q.mu.Lock() 73 | defer q.mu.Unlock() 74 | return len(q.requests) 75 | } 76 | -------------------------------------------------------------------------------- /internal/domain/email/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | // EmailService defines methods for sending emails, testing connections, 6 | // and managing email retry queues. 7 | type EmailService interface { 8 | // SendEmail sends an email based on the provided request. 9 | // It returns an error if the email could not be sent. 10 | // 11 | // Parameters: 12 | // - ctx Context: The context for managing timeouts and cancellations. 13 | // - request *EmailRequest: The email request containing necessary details for sending the email. 14 | // 15 | // Returns: 16 | // - error: An error indicating the failure to send the email, or nil if successful. 17 | SendEmail(ctx context.Context, request *EmailRequest) error 18 | 19 | // TestConnection tests the connection to the email service. 20 | // It returns an error if the connection test fails. 21 | // 22 | // Returns: 23 | // - error: An error indicating the failure of the connection test, or nil if successful. 24 | TestConnection() error 25 | 26 | // GetEmailRetryQueue retrieves the current email retry queue. 27 | // The retry queue contains emails that failed to send and are awaiting retry. 28 | // 29 | // Returns: 30 | // - *EmailRetryQueue: The current retry queue. If there are no failed emails, returns an empty queue. 31 | GetEmailRetryQueue() *EmailRetryQueue 32 | } 33 | -------------------------------------------------------------------------------- /internal/domain/jwks/model.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "crypto/rsa" 5 | "encoding/base64" 6 | "math/big" 7 | ) 8 | 9 | type Jwks struct { 10 | Keys []JWK `json:"keys"` 11 | } 12 | 13 | type JWK struct { 14 | Kty string `json:"kty"` // Key type (e.g., "RSA", "EC") 15 | Kid string `json:"kid"` // Key ID, used to identify the key 16 | Use string `json:"use"` // Public key use (e.g., "sig" for signature, "enc" for encryption) 17 | Alg string `json:"alg"` // Algorithm intended for use with the key (e.g., "RS256", "ES256") 18 | N string `json:"n,omitempty"` // RSA modulus (base64url-encoded) 19 | E string `json:"e,omitempty"` // RSA public exponent (base64url-encoded) 20 | X string `json:"x,omitempty"` // EC public key x-coordinate (base64url-encoded) 21 | Y string `json:"y,omitempty"` // EC public key y-coordinate (base64url-encoded) 22 | Crv string `json:"crv,omitempty"` // EC curve name (e.g., "P-256", "P-384", "P-521") 23 | } 24 | 25 | func NewJWK(keyID string, publicKey *rsa.PublicKey) JWK { 26 | return JWK{ 27 | Kty: "RSA", 28 | Kid: keyID, 29 | Use: "sig", 30 | Alg: "RS256", 31 | N: base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes()), 32 | E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes()), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/domain/jwt/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 7 | ) 8 | 9 | type JWTService interface { 10 | ParseWithClaims(ctx context.Context, tokenString string) (*token.TokenClaims, error) 11 | SignToken(ctx context.Context, claims *token.TokenClaims) (string, error) 12 | } 13 | -------------------------------------------------------------------------------- /internal/domain/login/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | user "github.com/vigiloauth/vigilo/v2/internal/domain/user" 7 | ) 8 | 9 | // LoginAttemptRepository defines the interface for storing and retrieving login attempts. 10 | type LoginAttemptRepository interface { 11 | 12 | // SaveLoginAttempt saves a login attempt. 13 | // 14 | // Parameters: 15 | // - ctx Context: The context for managing timeouts and cancellations. 16 | // - attempt *UserLoginAttempt: The login attempt to save. 17 | // 18 | // Returns: 19 | // - error: If an error occurs saving the login attempts. 20 | SaveLoginAttempt(ctx context.Context, attempt *user.UserLoginAttempt) error 21 | 22 | // GetLoginAttemptsByUserID retrieves all login attempts for a given user. 23 | // 24 | // Parameters: 25 | // - ctx Context: The context for managing timeouts and cancellations. 26 | // - userID string: The user ID. 27 | // 28 | // Returns: 29 | // - []*UserLoginAttempt: A slice of login attempts for the user. 30 | // - error: If an error occurs retrieving user login attempts. 31 | GetLoginAttemptsByUserID(ctx context.Context, userID string) ([]*user.UserLoginAttempt, error) 32 | } 33 | -------------------------------------------------------------------------------- /internal/domain/login/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/user" 7 | ) 8 | 9 | type LoginAttemptService interface { 10 | // SaveLoginAttempt logs a login attempt. 11 | // 12 | // Parameters: 13 | // - ctx Context: The context for managing timeouts and cancellations. 14 | // - attempt *UserLoginAttempt: The login attempt to save. 15 | SaveLoginAttempt(ctx context.Context, attempt *domain.UserLoginAttempt) error 16 | 17 | // GetLoginAttemptsByUserID retrieves all login attempts for a given user. 18 | // 19 | // Parameters: 20 | // - ctx Context: The context for managing timeouts and cancellations. 21 | // - userID string: The user ID. 22 | // 23 | // Returns: 24 | // - []*UserLoginAttempt: A slice of login attempts for the user. 25 | // - error: An error if retrieval fails. 26 | GetLoginAttemptsByUserID(ctx context.Context, userID string) ([]*domain.UserLoginAttempt, error) 27 | 28 | // HandleFailedLoginAttempt handles a failed login attempt. 29 | // It updates the user's last failed login time, saves the login attempt, and locks the account if necessary. 30 | // 31 | // Parameters: 32 | // - ctx Context: The context for managing timeouts and cancellations. 33 | // - user *User: The user who attempted to log in. 34 | // - attempt *UserLoginAttempt: The login attempt information. 35 | // 36 | // Returns: 37 | // - error: An error if an operation fails. 38 | HandleFailedLoginAttempt(ctx context.Context, user *domain.User, attempt *domain.UserLoginAttempt) error 39 | } 40 | -------------------------------------------------------------------------------- /internal/domain/oidc/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | jwks "github.com/vigiloauth/vigilo/v2/internal/domain/jwks" 7 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 8 | user "github.com/vigiloauth/vigilo/v2/internal/domain/user" 9 | ) 10 | 11 | type OIDCService interface { 12 | // GetUserInfo retrieves the user's profile information based on the claims 13 | // extracted from a validated access token. 14 | // 15 | // Parameters: 16 | // - ctx Context: The context for managing timeouts and cancellations. 17 | // - accessTokenClaims *TokenClaims: A pointer to TokenClaims that were parsed and validated 18 | // from the access token. These typically include standard OIDC claims such as 19 | // 'sub' (subject identifier), 'scope', 'exp' (expiration), etc. 20 | // 21 | // Returns: 22 | // - *UserInfoResponse: A pointer to a UserInfoResponse struct containing the requested user 23 | // information (e.g., name, email, profile picture), filtered according to the 24 | // authorized scopes. 25 | // - error: An error if the user cannot be found, the scopes are insufficient, or any 26 | // other issue occurs during retrieval. 27 | GetUserInfo(ctx context.Context, accessTokenClaims *token.TokenClaims) (*user.UserInfoResponse, error) 28 | 29 | // GetJwks retrieves the JSON Web Key Set (JWKS) used for verifying signatures 30 | // of tokens issued by the OpenID Connect provider. 31 | // 32 | // Parameters: 33 | // - ctx Context: The context for managing timeouts and cancellations. 34 | // 35 | // Returns: 36 | // - *Jwks: A pointer to a Jwks struct containing the public keys in JWKS format. 37 | GetJwks(ctx context.Context) *jwks.Jwks 38 | } 39 | -------------------------------------------------------------------------------- /internal/domain/session/manager.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type SessionManager interface { 9 | // GetUserIDFromSession checks if the user session is active based on the provided context and HTTP request. 10 | // 11 | // Parameters: 12 | // - ctx Context: The context for managing timeouts and cancellations. 13 | // - r *http.Request: The HTTP request associated with the user session. 14 | // 15 | // Returns: 16 | // - string: The user ID if the session is active, or an empty string if not. 17 | // - error: An error if the session data retrieval fails. 18 | GetUserIDFromSession(ctx context.Context, r *http.Request) (string, error) 19 | 20 | // GetUserAuthenticationTime retrieves the authentication time of the user session based on the provided context and HTTP request. 21 | // 22 | // Parameters: 23 | // - ctx Context: The context for managing timeouts and cancellations. 24 | // - r *http.Request: The HTTP request associated with the user session. 25 | // 26 | // Returns: 27 | // - int64: The authentication time in Unix timestamp format. 28 | // - error: An error if the session data retrieval fails. 29 | GetUserAuthenticationTime(ctx context.Context, r *http.Request) (int64, error) 30 | } 31 | -------------------------------------------------------------------------------- /internal/domain/session/model.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "time" 4 | 5 | // SessionData represents the data stored for a user's session. 6 | type SessionData struct { 7 | ID string // ID of the session. 8 | UserID string // ID of the user associated with the session. 9 | State string // Random string used to prevent CSRF attacks during Authorization Flow. 10 | ClientID string // ID of the OAuth 2.0 client application. 11 | ClientName string // Name of the OAuth 2.0 client application. 12 | IPAddress string // IP address of the user at the time of session creation. 13 | UserAgent string // User agent string of the user's browser or device. 14 | RedirectURI string // Redirect URI from OAuth 2.0 client application. 15 | ExpirationTime time.Time // The timestamp when the session expires. 16 | AuthenticationTime int64 // The timestamp of when the user was last authenticated 17 | } 18 | -------------------------------------------------------------------------------- /internal/domain/session/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // SessionRepository defines the interface for session management operations. 8 | type SessionRepository interface { 9 | // SaveSession creates a new session and returns the session ID. 10 | // 11 | // Parameters: 12 | // - ctx Context: The context for managing timeouts and cancellations. 13 | // - sessionData SessionData: The data to store in the new session. 14 | // 15 | // Returns: 16 | // - error: An error if the session creation fails. 17 | SaveSession(ctx context.Context, sessionData *SessionData) error 18 | 19 | // GetSessionByID retrieves session data for a given session ID. 20 | // 21 | // Parameters: 22 | // - ctx Context: The context for managing timeouts and cancellations. 23 | // - sessionID string: The unique identifier of the session to retrieve. 24 | // 25 | // Returns: 26 | // - *SessionData: The session data associated with the session ID. 27 | // - error: An error if the session is not found or retrieval fails. 28 | GetSessionByID(ctx context.Context, sessionID string) (*SessionData, error) 29 | 30 | // UpdateSessionByID updates the session data for a given session ID. 31 | // 32 | // Parameters: 33 | // - ctx Context: The context for managing timeouts and cancellations. 34 | // - sessionID string: The unique identifier of the session to update. 35 | // - sessionData SessionData: The updated session data. 36 | // 37 | // Returns: 38 | // - error: An error if the update fails. 39 | UpdateSessionByID(ctx context.Context, sessionID string, sessionData *SessionData) error 40 | 41 | // DeleteSessionByID removes a session with the given session ID. 42 | // 43 | // Parameters: 44 | // - ctx Context: The context for managing timeouts and cancellations. 45 | // - sessionID string: The unique identifier of the session to delete. 46 | // 47 | // Returns: 48 | // - error: An error if the deletion fails. 49 | DeleteSessionByID(ctx context.Context, sessionID string) error 50 | } 51 | -------------------------------------------------------------------------------- /internal/domain/session/service.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // SessionService defines the interface for session management. 8 | type SessionService interface { 9 | // CreateSession creates a new session token and sets it in an HttpOnly cookie. 10 | // 11 | // Parameters: 12 | // - w http.ResponseWriter: The HTTP response writer. 13 | // - r *http.Request: The HTTP request. 14 | // - userID string: The user's ID address. 15 | // - sessionExpiration time.Duration: The session expiration time. 16 | // 17 | // Returns: 18 | // - error: An error if token generation or cookie setting fails. 19 | CreateSession(w http.ResponseWriter, r *http.Request, sessionData *SessionData) error 20 | 21 | // InvalidateSession invalidates the session token by adding it to the blacklist. 22 | // 23 | // Parameters: 24 | // - w http.ResponseWriter: The HTTP response writer. 25 | // - r *http.Request: The HTTP request. 26 | // 27 | // Returns: 28 | // - error: An error if token parsing or blacklist addition fails. 29 | InvalidateSession(w http.ResponseWriter, r *http.Request) error 30 | 31 | // GetUserIDFromSession retrieves the user ID from the current session. 32 | // 33 | // Parameters: 34 | // - r *http.Request: The HTTP request. 35 | // 36 | // Returns: 37 | // - string: The user ID. 38 | // - error: If an error occurs retrieving the user ID from the session. 39 | GetUserIDFromSession(r *http.Request) (string, error) 40 | 41 | // UpdateSession updates the current session. 42 | // 43 | // Parameters: 44 | // - r *http.Request: The HTTP request. 45 | // - sessionData *SessionData: The sessionData to update. 46 | // 47 | // Returns: 48 | // - error: If an error occurs during the update. 49 | UpdateSession(r *http.Request, sessionData *SessionData) error 50 | 51 | // GetSessionData retrieves the current session. 52 | // 53 | // Parameters: 54 | // - r *http.Request: The HTTP request. 55 | // 56 | // Returns: 57 | // - *SessionData: The session data is successful. 58 | // - error: An error if retrieval fails. 59 | GetSessionData(r *http.Request) (*SessionData, error) 60 | } 61 | -------------------------------------------------------------------------------- /internal/domain/token/issuer.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/claims" 8 | "github.com/vigiloauth/vigilo/v2/internal/types" 9 | ) 10 | 11 | type TokenIssuer interface { 12 | IssueAccessToken(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string) (string, error) 13 | IssueTokenPair(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string, claims *domain.ClaimsRequest) (string, string, error) 14 | IssueIDToken(ctx context.Context, subject string, audience string, scopes types.Scope, nonce string, acrValues string, authTime time.Time) (string, error) 15 | } 16 | -------------------------------------------------------------------------------- /internal/domain/token/parser.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type TokenParser interface { 6 | // ParseToken parses a JWT token string into TokenClaims. 7 | // 8 | // Parameters: 9 | // - ctx ctx.Context: Context for the request, containing the request ID for logging. 10 | // - tokenString string: The JWT token string to parse and validate. 11 | // 12 | // Returns: 13 | // - *token.TokenClaims: The parsed token claims if successful. 14 | // - error: An error if token parsing, decryption, or validation fails. 15 | ParseToken(ctx context.Context, tokenString string) (*TokenClaims, error) 16 | } 17 | -------------------------------------------------------------------------------- /internal/domain/token/validation.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/vigiloauth/vigilo/v2/internal/errors" 8 | ) 9 | 10 | func (t *TokenRequest) ValidateCodeVerifier() error { 11 | codeVerifierLength := len(t.CodeVerifier) 12 | if codeVerifierLength < 43 || codeVerifierLength > 128 { 13 | return errors.New(errors.ErrCodeInvalidRequest, fmt.Sprintf("invalid code verifier length (%d): must be between 43 and 128 characters", codeVerifierLength)) 14 | } 15 | 16 | validCodeVerifierRegex := regexp.MustCompile(`^[A-Za-z0-9._~-]+$`) 17 | if !validCodeVerifierRegex.MatchString(t.CodeVerifier) { 18 | return errors.New(errors.ErrCodeInvalidRequest, "invalid characters: only A-Z, a-z, 0-9, '-', and '_' are allowed (Base64 URL encoding)") 19 | } 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/domain/token/validator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type TokenValidator interface { 6 | 7 | // ValidateToken checks to see if a token is blacklisted or expired. 8 | // 9 | // Parameters: 10 | // - ctx Context: The context for managing timeouts and cancellations. 11 | // - tokenStr string: The token string to check. 12 | // 13 | // Returns: 14 | // - error: An error if the token is blacklisted or expired. 15 | ValidateToken(ctx context.Context, tokenStr string) error 16 | } 17 | -------------------------------------------------------------------------------- /internal/domain/user/authenticator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type UserAuthenticator interface { 6 | // AuthenticateUserWithRequest authenticates a user based on a login request and request metadata. 7 | // 8 | // This method constructs a User object and a UserLoginAttempt object from the provided 9 | // login request and HTTP request metadata, then delegates the authentication process 10 | // to the AuthenticateUser method. 11 | // 12 | // Parameters: 13 | // - ctx Context: The context for managing timeouts, cancellations, and for retrieving/storing request metadata. 14 | // - request *UserLoginRequest: The login request containing the user's email and password. 15 | // 16 | // Returns: 17 | // - *UserLoginResponse: The response containing user information and a JWT token if authentication is successful. 18 | // - error: An error if authentication fails or if the input is invalid. 19 | AuthenticateUser(ctx context.Context, request *UserLoginRequest) (*UserLoginResponse, error) 20 | } 21 | -------------------------------------------------------------------------------- /internal/domain/user/creator.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type UserCreator interface { 6 | // CreateUser creates a new user in the system. 7 | // 8 | // Parameters: 9 | // - ctx Context: The context for managing timeouts and cancellations. 10 | // - user *User: The user to register. 11 | // 12 | // Returns: 13 | // - *UserRegistrationResponse: The registered user object and an access token. 14 | // - error: An error if any occurred during the process. 15 | CreateUser(ctx context.Context, user *User) (*UserRegistrationResponse, error) 16 | } 17 | -------------------------------------------------------------------------------- /internal/domain/user/manager.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type UserManager interface { 6 | // GetUserByUsername retrieves a user using their username. 7 | // 8 | // Parameter: 9 | // - ctx Context: The context for managing timeouts and cancellations. 10 | // - username string: The username of the user to retrieve. 11 | // 12 | // Returns: 13 | // - *User: The retrieved user, otherwise nil. 14 | // - error: If an error occurs retrieving the user. 15 | GetUserByUsername(ctx context.Context, username string) (*User, error) 16 | 17 | // GetUserByID retrieves a user from the store using their ID. 18 | // 19 | // Parameters: 20 | // - ctx Context: The context for managing timeouts and cancellations. 21 | // - userID string: The ID used to retrieve the user. 22 | // 23 | // Returns: 24 | // - *User: The User object if found, or nil if not found. 25 | // - error: If an error occurs retrieving the user. 26 | GetUserByID(ctx context.Context, userID string) (*User, error) 27 | 28 | // DeleteUnverifiedUsers deletes any user that hasn't verified their account and 29 | // has been created for over a week. 30 | // 31 | // Parameter: 32 | // - ctx Context: The context for managing timeouts and cancellations. 33 | // 34 | // Returns: 35 | // - error: an error if deletion fails, otherwise nil. 36 | DeleteUnverifiedUsers(ctx context.Context) error 37 | 38 | // ResetPassword resets the user's password using the provided reset token. 39 | // 40 | // Parameters: 41 | // - ctx Context: The context for managing timeouts and cancellations. 42 | // - userEmail string: The user's email address. 43 | // - newPassword string: The new password. 44 | // - resetToken string: The reset token. 45 | // 46 | // Returns: 47 | // - *users.UserPasswordResetResponse: A response message. 48 | // - error: An error if the operation fails. 49 | ResetPassword(ctx context.Context, userEmail, newPassword, resetToken string) (*UserPasswordResetResponse, error) 50 | } 51 | -------------------------------------------------------------------------------- /internal/domain/user/verifier.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | type UserVerifier interface { 6 | // VerifyEmailAddress validates the verification code and marks the user's email as verified. 7 | // 8 | // Parameter: 9 | // - ctx Context: The context for managing timeouts and cancellations. 10 | // - verificationCode string: The verification code to verify. 11 | // 12 | // Returns: 13 | // - error: an error if validation fails, otherwise nil. 14 | VerifyEmailAddress(ctx context.Context, verificationCode string) error 15 | } 16 | -------------------------------------------------------------------------------- /internal/domain/userconsent/model.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/vigiloauth/vigilo/v2/internal/types" 7 | ) 8 | 9 | // UserConsentRecord represents a user's consent for a specific client 10 | type UserConsentRecord struct { 11 | UserID string 12 | ClientID string 13 | Scope types.Scope 14 | CreatedAt time.Time 15 | } 16 | 17 | type UserConsentResponse struct { 18 | Approved bool `json:"approved"` 19 | ClientID string `json:"client_id"` 20 | ClientName string `json:"client_name"` 21 | RedirectURI string `json:"redirect_uri"` 22 | Scopes []types.Scope `json:"scopes"` 23 | ConsentEndpoint string `json:"consent_endpoint"` 24 | State string `json:"state"` 25 | Error string `json:"error,omitempty"` 26 | Success bool `json:"success,omitempty"` 27 | } 28 | 29 | type UserConsentRequest struct { 30 | Approved bool `json:"approved"` 31 | Scopes []types.Scope `json:"scopes,omitempty"` 32 | ResponseType string `json:"response_type,omitempty"` 33 | State string `json:"state,omitempty"` 34 | Nonce string `json:"nonce,omitempty"` 35 | Display string `json:"display,omitempty"` 36 | } 37 | -------------------------------------------------------------------------------- /internal/domain/userconsent/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vigiloauth/vigilo/v2/internal/types" 7 | ) 8 | 9 | // UserConsentRepository defines the interface for storing and managing user consent data. 10 | type UserConsentRepository interface { 11 | // HasConsent checks if a user has granted consent to a client for specific scopes. 12 | // 13 | // Parameters: 14 | // - ctx Context: The context for managing timeouts and cancellations. 15 | // - userID string: The ID of the user. 16 | // - clientID string: The ID of the client application. 17 | // - requestedScope string: The requested scope(s). 18 | // 19 | // Returns: 20 | // bool: True if consent exists, false otherwise. 21 | // error: An error if the check fails, or nil if successful. 22 | HasConsent(ctx context.Context, userID string, clientID string, requestedScope types.Scope) (bool, error) 23 | 24 | // SaveConsent stores a user's consent for a client and scope. 25 | // 26 | // Parameters: 27 | // - ctx Context: The context for managing timeouts and cancellations. 28 | // - userID string: The ID of the user. 29 | // - clientID string: The ID of the client application. 30 | // - scope string: The granted scope(s). 31 | // 32 | // Returns: 33 | // - error: An error if the consent cannot be saved, or nil if successful. 34 | SaveConsent(ctx context.Context, userID string, clientID string, scope types.Scope) error 35 | 36 | // RevokeConsent removes a user's consent for a client. 37 | // 38 | // Parameters: 39 | // - ctx Context: The context for managing timeouts and cancellations. 40 | // - userID string: The ID of the user. 41 | // - clientID string: The ID of the client application. 42 | // 43 | // Returns: 44 | // - error: An error if the consent cannot be revoked, or nil if successful. 45 | RevokeConsent(ctx context.Context, userID string, clientID string) error 46 | } 47 | -------------------------------------------------------------------------------- /internal/errors/collection.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "fmt" 4 | 5 | type ErrorCollection struct { 6 | errors []error 7 | } 8 | 9 | // NewErrorCollection creates a new collection of errors 10 | func NewErrorCollection() *ErrorCollection { 11 | return &ErrorCollection{ 12 | errors: []error{}, 13 | } 14 | } 15 | 16 | // Add adds an error to the collection 17 | func (ec *ErrorCollection) Add(err error) { 18 | ec.errors = append(ec.errors, err) 19 | } 20 | 21 | // Errors returns the list of validation errors 22 | func (ec *ErrorCollection) Errors() *[]error { 23 | return &ec.errors 24 | } 25 | 26 | // HasErrors checks if there are any validation errors 27 | func (ec *ErrorCollection) HasErrors() bool { 28 | return len(ec.errors) > 0 29 | } 30 | 31 | // Error implements the error interface 32 | func (ec *ErrorCollection) Error() string { 33 | return fmt.Sprintf("%d errors", len(ec.errors)) 34 | } 35 | -------------------------------------------------------------------------------- /internal/middleware/rate_limiter.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/vigiloauth/vigilo/v2/idp/config" 8 | ) 9 | 10 | // RateLimiter implements a token bucket rate limiting algorithm. 11 | // RateLimiter implements a token bucket rate limiting algorithm. 12 | type RateLimiter struct { 13 | rate float64 // Number of tokens to add per second 14 | capacity float64 // Maximum number of tokens that can be stored 15 | tokens float64 // Current number of available tokens 16 | lastUpdate time.Time // Timestamp of the last token update 17 | mu sync.Mutex // Mutex to protect concurrent access 18 | 19 | logger *config.Logger 20 | module string 21 | } 22 | 23 | // NewRateLimiter creates a new RateLimiter instance. 24 | func NewRateLimiter(rate int) *RateLimiter { 25 | return &RateLimiter{ 26 | rate: float64(rate), 27 | capacity: float64(rate), 28 | tokens: float64(rate), 29 | lastUpdate: time.Now(), 30 | logger: config.GetServerConfig().Logger(), 31 | module: "Rate Limiter", 32 | } 33 | } 34 | 35 | // Allow checks if a request is allowed based on the rate limit. 36 | func (rl *RateLimiter) Allow(requestID string) bool { 37 | rl.logger.Debug(rl.module, requestID, "[Allow]: Verifying if the request exceeds the rate limit") 38 | rl.mu.Lock() 39 | defer rl.mu.Unlock() 40 | 41 | now := time.Now() 42 | elapsed := now.Sub(rl.lastUpdate).Seconds() 43 | rl.lastUpdate = now 44 | 45 | // Add tokens based on the elapsed time 46 | rl.tokens += elapsed * rl.rate 47 | 48 | // Cap tokens at the maximum capacity 49 | if rl.tokens > rl.capacity { 50 | rl.tokens = rl.capacity 51 | } 52 | 53 | if rl.tokens >= 1.0 { 54 | rl.logger.Debug(rl.module, requestID, "[Allow]: Request is valid") 55 | rl.tokens -= 1.0 // Consume a token 56 | return true 57 | } 58 | 59 | rl.logger.Warn(rl.module, requestID, "[Allow]: Request is invalid as it exceeds the rate limit") 60 | return false 61 | } 62 | -------------------------------------------------------------------------------- /internal/middleware/rate_limiter_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | const testRequestID string = "requestID" 11 | 12 | func TestRateLimiter_Allow(t *testing.T) { 13 | rate := 2 14 | rl := NewRateLimiter(rate) 15 | assert.True(t, rl.Allow(testRequestID)) 16 | assert.True(t, rl.Allow(testRequestID)) 17 | assert.False(t, rl.Allow(testRequestID)) 18 | } 19 | 20 | func TestRateLimiter_TokenRefill(t *testing.T) { 21 | rate := 1 22 | rl := NewRateLimiter(rate) 23 | assert.True(t, rl.Allow(testRequestID)) 24 | assert.False(t, rl.Allow(testRequestID)) 25 | time.Sleep(2 * time.Second) 26 | assert.True(t, rl.Allow(testRequestID)) 27 | } 28 | -------------------------------------------------------------------------------- /internal/mocks/audit/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/audit" 8 | ) 9 | 10 | var _ domain.AuditRepository = (*MockAuditRepository)(nil) 11 | 12 | type MockAuditRepository struct { 13 | StoreAuditEventFunc func(ctx context.Context, event *domain.AuditEvent) error 14 | GetAuditEventsFunc func(ctx context.Context, filters map[string]any, from time.Time, to time.Time, limit, offset int) ([]*domain.AuditEvent, error) 15 | DeleteEventFunc func(ctx context.Context, eventID string) error 16 | } 17 | 18 | func (m *MockAuditRepository) StoreAuditEvent(ctx context.Context, event *domain.AuditEvent) error { 19 | return m.StoreAuditEventFunc(ctx, event) 20 | } 21 | 22 | func (m *MockAuditRepository) GetAuditEvents(ctx context.Context, filters map[string]any, from time.Time, to time.Time, limit, offset int) ([]*domain.AuditEvent, error) { 23 | return m.GetAuditEventsFunc(ctx, filters, from, to, limit, offset) 24 | } 25 | 26 | func (m *MockAuditRepository) DeleteEvent(ctx context.Context, eventID string) error { 27 | return m.DeleteEventFunc(ctx, eventID) 28 | } 29 | -------------------------------------------------------------------------------- /internal/mocks/audit/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/audit" 8 | ) 9 | 10 | var _ domain.AuditLogger = (*MockAuditLogger)(nil) 11 | 12 | type MockAuditLogger struct { 13 | StoreEventFunc func(ctx context.Context, eventType domain.EventType, success bool, action domain.ActionType, method domain.MethodType, err error) 14 | DeleteOldEventsFunc func(cts context.Context, olderThan time.Time) error 15 | GetAuditEventsFunc func(ctx context.Context, filters map[string]any, from string, to string, limit, offset int) ([]*domain.AuditEvent, error) 16 | } 17 | 18 | func (m *MockAuditLogger) StoreEvent(ctx context.Context, eventType domain.EventType, success bool, action domain.ActionType, method domain.MethodType, err error) { 19 | m.StoreEventFunc(ctx, eventType, success, action, method, err) 20 | } 21 | 22 | func (m *MockAuditLogger) DeleteOldEvents(ctx context.Context, olderThan time.Time) error { 23 | return m.DeleteOldEventsFunc(ctx, olderThan) 24 | } 25 | 26 | func (m *MockAuditLogger) GetAuditEvents(ctx context.Context, filters map[string]any, from string, to string, limit, offset int) ([]*domain.AuditEvent, error) { 27 | return m.GetAuditEventsFunc(ctx, filters, from, to, limit, offset) 28 | } 29 | -------------------------------------------------------------------------------- /internal/mocks/authorization/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | authz "github.com/vigiloauth/vigilo/v2/internal/domain/authorization" 7 | authzCode "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 8 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 9 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 10 | user "github.com/vigiloauth/vigilo/v2/internal/domain/user" 11 | ) 12 | 13 | var _ authz.AuthorizationService = (*MockAuthorizationService)(nil) 14 | 15 | type MockAuthorizationService struct { 16 | AuthorizeClientFunc func(ctx context.Context, authorizationRequest *client.ClientAuthorizationRequest) (string, error) 17 | AuthorizeTokenExchangeFunc func(ctx context.Context, tokenRequest *token.TokenRequest) (*authzCode.AuthorizationCodeData, error) 18 | AuthorizeUserInfoRequestFunc func(ctx context.Context, accessTokenClaims *token.TokenClaims) (*user.User, error) 19 | UpdateAuthorizationCodeFunc func(ctx context.Context, authzCode *authzCode.AuthorizationCodeData) error 20 | } 21 | 22 | func (m *MockAuthorizationService) AuthorizeClient(ctx context.Context, authorizationRequest *client.ClientAuthorizationRequest) (string, error) { 23 | return m.AuthorizeClientFunc(ctx, authorizationRequest) 24 | } 25 | 26 | func (m *MockAuthorizationService) AuthorizeTokenExchange(ctx context.Context, tokenRequest *token.TokenRequest) (*authzCode.AuthorizationCodeData, error) { 27 | return m.AuthorizeTokenExchangeFunc(ctx, tokenRequest) 28 | } 29 | 30 | func (m *MockAuthorizationService) AuthorizeUserInfoRequest(ctx context.Context, accessTokenClaims *token.TokenClaims) (*user.User, error) { 31 | return m.AuthorizeUserInfoRequestFunc(ctx, accessTokenClaims) 32 | } 33 | 34 | func (m *MockAuthorizationService) UpdateAuthorizationCode(ctx context.Context, authData *authzCode.AuthorizationCodeData) error { 35 | return m.UpdateAuthorizationCodeFunc(ctx, authData) 36 | } 37 | -------------------------------------------------------------------------------- /internal/mocks/authzcode/creator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 7 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 8 | ) 9 | 10 | var _ domain.AuthorizationCodeCreator = (*MockAuthorizationCodeCreator)(nil) 11 | 12 | type MockAuthorizationCodeCreator struct { 13 | GenerateAuthorizationCodeFunc func(ctx context.Context, request *client.ClientAuthorizationRequest) (string, error) 14 | } 15 | 16 | func (m *MockAuthorizationCodeCreator) GenerateAuthorizationCode(ctx context.Context, request *client.ClientAuthorizationRequest) (string, error) { 17 | return m.GenerateAuthorizationCodeFunc(ctx, request) 18 | } 19 | -------------------------------------------------------------------------------- /internal/mocks/authzcode/issuer.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 7 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 8 | ) 9 | 10 | var _ domain.AuthorizationCodeIssuer = (*MockAuthorizationCodeIssuer)(nil) 11 | 12 | type MockAuthorizationCodeIssuer struct { 13 | IssueAuthorizationCodeFunc func(ctx context.Context, req *client.ClientAuthorizationRequest) (string, error) 14 | } 15 | 16 | func (m *MockAuthorizationCodeIssuer) IssueAuthorizationCode(ctx context.Context, req *client.ClientAuthorizationRequest) (string, error) { 17 | return m.IssueAuthorizationCodeFunc(ctx, req) 18 | } 19 | -------------------------------------------------------------------------------- /internal/mocks/authzcode/manager.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | authzCode "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 7 | ) 8 | 9 | var _ authzCode.AuthorizationCodeManager = (*MockAuthorizationCodeManager)(nil) 10 | 11 | type MockAuthorizationCodeManager struct { 12 | RevokeAuthorizationCodeFunc func(ctx context.Context, code string) error 13 | GetAuthorizationCodeFunc func(ctx context.Context, code string) (*authzCode.AuthorizationCodeData, error) 14 | UpdateAuthorizationCodeFunc func(ctx context.Context, authData *authzCode.AuthorizationCodeData) error 15 | } 16 | 17 | func (m *MockAuthorizationCodeManager) RevokeAuthorizationCode(ctx context.Context, code string) error { 18 | return m.RevokeAuthorizationCodeFunc(ctx, code) 19 | } 20 | 21 | func (m *MockAuthorizationCodeManager) GetAuthorizationCode(ctx context.Context, code string) (*authzCode.AuthorizationCodeData, error) { 22 | return m.GetAuthorizationCodeFunc(ctx, code) 23 | } 24 | 25 | func (m *MockAuthorizationCodeManager) UpdateAuthorizationCode(ctx context.Context, authData *authzCode.AuthorizationCodeData) error { 26 | return m.UpdateAuthorizationCodeFunc(ctx, authData) 27 | } 28 | -------------------------------------------------------------------------------- /internal/mocks/authzcode/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | authz "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 8 | ) 9 | 10 | var _ authz.AuthorizationCodeRepository = (*MockAuthorizationCodeRepository)(nil) 11 | 12 | type MockAuthorizationCodeRepository struct { 13 | StoreAuthorizationCodeFunc func(ctx context.Context, code string, data *authz.AuthorizationCodeData, expiresAt time.Time) error 14 | GetAuthorizationCodeFunc func(ctx context.Context, code string) (*authz.AuthorizationCodeData, error) 15 | DeleteAuthorizationCodeFunc func(ctx context.Context, code string) error 16 | UpdateAuthorizationCodeFunc func(ctx context.Context, code string, authData *authz.AuthorizationCodeData) error 17 | } 18 | 19 | func (m *MockAuthorizationCodeRepository) StoreAuthorizationCode(ctx context.Context, code string, data *authz.AuthorizationCodeData, expiresAt time.Time) error { 20 | return m.StoreAuthorizationCodeFunc(ctx, code, data, expiresAt) 21 | } 22 | 23 | func (m *MockAuthorizationCodeRepository) GetAuthorizationCode(ctx context.Context, code string) (*authz.AuthorizationCodeData, error) { 24 | return m.GetAuthorizationCodeFunc(ctx, code) 25 | } 26 | 27 | func (m *MockAuthorizationCodeRepository) DeleteAuthorizationCode(ctx context.Context, code string) error { 28 | return m.DeleteAuthorizationCodeFunc(ctx, code) 29 | } 30 | 31 | func (m *MockAuthorizationCodeRepository) UpdateAuthorizationCode(ctx context.Context, code string, authData *authz.AuthorizationCodeData) error { 32 | return m.UpdateAuthorizationCodeFunc(ctx, code, authData) 33 | } 34 | -------------------------------------------------------------------------------- /internal/mocks/authzcode/validator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 7 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 8 | ) 9 | 10 | var _ domain.AuthorizationCodeValidator = (*MockAuthorizationCodeRequestValidator)(nil) 11 | 12 | type MockAuthorizationCodeRequestValidator struct { 13 | ValidateRequestFunc func(ctx context.Context, req *client.ClientAuthorizationRequest) error 14 | ValidateAuthorizationCodeFunc func(ctx context.Context, code, clientID, redirectURI string) error 15 | ValidatePKCEFunc func(ctx context.Context, authzCodeData *domain.AuthorizationCodeData, codeVerifier string) error 16 | } 17 | 18 | func (m *MockAuthorizationCodeRequestValidator) ValidateRequest(ctx context.Context, req *client.ClientAuthorizationRequest) error { 19 | return m.ValidateRequestFunc(ctx, req) 20 | } 21 | 22 | func (m *MockAuthorizationCodeRequestValidator) ValidateAuthorizationCode(ctx context.Context, code, clientID, redirectURI string) error { 23 | return m.ValidateAuthorizationCodeFunc(ctx, code, clientID, redirectURI) 24 | } 25 | 26 | func (m *MockAuthorizationCodeRequestValidator) ValidatePKCE(ctx context.Context, authzCodeData *domain.AuthorizationCodeData, codeVerifier string) error { 27 | return m.ValidatePKCEFunc(ctx, authzCodeData, codeVerifier) 28 | } 29 | -------------------------------------------------------------------------------- /internal/mocks/client/authenticator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/client" 8 | "github.com/vigiloauth/vigilo/v2/internal/types" 9 | ) 10 | 11 | var _ domain.ClientAuthenticator = (*MockClientAuthenticator)(nil) 12 | 13 | type MockClientAuthenticator struct { 14 | AuthenticateRequestFunc func(ctx context.Context, r *http.Request, requiredScope types.Scope) error 15 | AuthenticateClientFunc func(ctx context.Context, req *domain.ClientAuthenticationRequest) error 16 | } 17 | 18 | func (m *MockClientAuthenticator) AuthenticateRequest(ctx context.Context, r *http.Request, requiredScope types.Scope) error { 19 | return m.AuthenticateRequestFunc(ctx, r, requiredScope) 20 | } 21 | 22 | func (m *MockClientAuthenticator) AuthenticateClient(ctx context.Context, req *domain.ClientAuthenticationRequest) error { 23 | return m.AuthenticateClientFunc(ctx, req) 24 | } 25 | -------------------------------------------------------------------------------- /internal/mocks/client/authorization.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | var _ domain.ClientAuthorization = (*MockClientAuthorization)(nil) 10 | 11 | type MockClientAuthorization struct { 12 | AuthorizeFunc func(ctx context.Context, request *domain.ClientAuthorizationRequest) (string, error) 13 | } 14 | 15 | func (m *MockClientAuthorization) Authorize(ctx context.Context, request *domain.ClientAuthorizationRequest) (string, error) { 16 | return m.AuthorizeFunc(ctx, request) 17 | } 18 | -------------------------------------------------------------------------------- /internal/mocks/client/creator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | var _ domain.ClientCreator = (*MockClientCreator)(nil) 10 | 11 | type MockClientCreator struct { 12 | RegisterFunc func(ctx context.Context, client *domain.ClientRegistrationRequest) (*domain.ClientRegistrationResponse, error) 13 | } 14 | 15 | func (m *MockClientCreator) Register(ctx context.Context, client *domain.ClientRegistrationRequest) (*domain.ClientRegistrationResponse, error) { 16 | return m.RegisterFunc(ctx, client) 17 | } 18 | -------------------------------------------------------------------------------- /internal/mocks/client/manager.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | var _ domain.ClientManager = (*MockClientManager)(nil) 10 | 11 | type MockClientManager struct { 12 | RegenerateClientSecretFunc func(ctx context.Context, clientID string) (*domain.ClientSecretRegenerationResponse, error) 13 | GetClientByIDFunc func(ctx context.Context, clientID string) (*domain.Client, error) 14 | GetClientInformationFunc func(ctx context.Context, clientID string, registrationAccessToken string) (*domain.ClientInformationResponse, error) 15 | UpdateClientInformationFunc func(ctx context.Context, clientID string, registrationAccessToken string, req *domain.ClientUpdateRequest) (*domain.ClientInformationResponse, error) 16 | DeleteClientInformationFunc func(ctx context.Context, clientID string, registrationAccessToken string) error 17 | } 18 | 19 | func (m *MockClientManager) RegenerateClientSecret(ctx context.Context, clientID string) (*domain.ClientSecretRegenerationResponse, error) { 20 | return m.RegenerateClientSecretFunc(ctx, clientID) 21 | } 22 | 23 | func (m *MockClientManager) GetClientByID(ctx context.Context, clientID string) (*domain.Client, error) { 24 | return m.GetClientByIDFunc(ctx, clientID) 25 | } 26 | 27 | func (m *MockClientManager) GetClientInformation(ctx context.Context, clientID string, registrationAccessToken string) (*domain.ClientInformationResponse, error) { 28 | return m.GetClientInformationFunc(ctx, clientID, registrationAccessToken) 29 | } 30 | 31 | func (m *MockClientManager) UpdateClientInformation(ctx context.Context, clientID string, registrationAccessToken string, req *domain.ClientUpdateRequest) (*domain.ClientInformationResponse, error) { 32 | return m.UpdateClientInformationFunc(ctx, clientID, registrationAccessToken, req) 33 | } 34 | 35 | func (m *MockClientManager) DeleteClientInformation(ctx context.Context, clientID string, registrationAccessToken string) error { 36 | return m.DeleteClientInformationFunc(ctx, clientID, registrationAccessToken) 37 | } 38 | -------------------------------------------------------------------------------- /internal/mocks/client/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | var _ client.ClientRepository = (*MockClientRepository)(nil) 10 | 11 | type MockClientRepository struct { 12 | SaveClientFunc func(ctx context.Context, client *client.Client) error 13 | GetClientByIDFunc func(ctx context.Context, clientID string) (*client.Client, error) 14 | DeleteClientByIDFunc func(ctx context.Context, clientID string) error 15 | UpdateClientFunc func(ctx context.Context, client *client.Client) error 16 | IsExistingIDFunc func(ctx context.Context, clientID string) bool 17 | } 18 | 19 | func (m *MockClientRepository) SaveClient(ctx context.Context, client *client.Client) error { 20 | return m.SaveClientFunc(ctx, client) 21 | } 22 | 23 | func (m *MockClientRepository) GetClientByID(ctx context.Context, clientID string) (*client.Client, error) { 24 | return m.GetClientByIDFunc(ctx, clientID) 25 | } 26 | 27 | func (m *MockClientRepository) DeleteClientByID(ctx context.Context, clientID string) error { 28 | return m.DeleteClientByIDFunc(ctx, clientID) 29 | } 30 | 31 | func (m *MockClientRepository) UpdateClient(ctx context.Context, client *client.Client) error { 32 | return m.UpdateClientFunc(ctx, client) 33 | } 34 | 35 | func (m *MockClientRepository) IsExistingID(ctx context.Context, clientID string) bool { 36 | return m.IsExistingIDFunc(ctx, clientID) 37 | } 38 | -------------------------------------------------------------------------------- /internal/mocks/client/validator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/client" 7 | ) 8 | 9 | var _ domain.ClientValidator = (*MockClientValidator)(nil) 10 | 11 | type MockClientValidator struct { 12 | ValidateRegistrationRequestFunc func(ctx context.Context, req *domain.ClientRegistrationRequest) error 13 | ValidateUpdateRequestFunc func(ctx context.Context, req *domain.ClientUpdateRequest) error 14 | ValidateAuthorizationRequestFunc func(ctx context.Context, req *domain.ClientAuthorizationRequest) error 15 | ValidateRedirectURIFunc func(ctx context.Context, redirectURI string, existingClient *domain.Client) error 16 | ValidateClientAndRegistrationAccessTokenFunc func(ctx context.Context, clientID string, registrationAccessToken string) error 17 | } 18 | 19 | func (m *MockClientValidator) ValidateRegistrationRequest(ctx context.Context, req *domain.ClientRegistrationRequest) error { 20 | return m.ValidateRegistrationRequestFunc(ctx, req) 21 | } 22 | 23 | func (m *MockClientValidator) ValidateUpdateRequest(ctx context.Context, req *domain.ClientUpdateRequest) error { 24 | return m.ValidateUpdateRequestFunc(ctx, req) 25 | } 26 | 27 | func (m *MockClientValidator) ValidateAuthorizationRequest(ctx context.Context, req *domain.ClientAuthorizationRequest) error { 28 | return m.ValidateAuthorizationRequestFunc(ctx, req) 29 | } 30 | 31 | func (m *MockClientValidator) ValidateRedirectURI(ctx context.Context, redirectURI string, existingClient *domain.Client) error { 32 | return m.ValidateRedirectURIFunc(ctx, redirectURI, existingClient) 33 | } 34 | 35 | func (m *MockClientValidator) ValidateClientAndRegistrationAccessToken(ctx context.Context, clientID string, registrationAccessToken string) error { 36 | return m.ValidateClientAndRegistrationAccessTokenFunc(ctx, clientID, registrationAccessToken) 37 | } 38 | -------------------------------------------------------------------------------- /internal/mocks/cookies/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/cookies" 9 | ) 10 | 11 | var _ domain.HTTPCookieService = (*MockHTTPCookieService)(nil) 12 | 13 | type MockHTTPCookieService struct { 14 | SetSessionCookieFunc func(ctx context.Context, w http.ResponseWriter, token string, expirationTime time.Duration) 15 | ClearSessionCookieFunc func(ctx context.Context, w http.ResponseWriter) 16 | GetSessionCookieFunc func(r *http.Request) (*http.Cookie, error) 17 | } 18 | 19 | func (m *MockHTTPCookieService) SetSessionCookie(ctx context.Context, w http.ResponseWriter, token string, expirationTime time.Duration) { 20 | m.SetSessionCookieFunc(ctx, w, token, expirationTime) 21 | } 22 | 23 | func (m *MockHTTPCookieService) ClearSessionCookie(ctx context.Context, w http.ResponseWriter) { 24 | m.ClearSessionCookieFunc(ctx, w) 25 | } 26 | 27 | func (m *MockHTTPCookieService) GetSessionCookie(r *http.Request) (*http.Cookie, error) { 28 | return m.GetSessionCookieFunc(r) 29 | } 30 | -------------------------------------------------------------------------------- /internal/mocks/crypto/cryptographer.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import domain "github.com/vigiloauth/vigilo/v2/internal/domain/crypto" 4 | 5 | var _ domain.Cryptographer = (*MockCryptographer)(nil) 6 | 7 | type MockCryptographer struct { 8 | EncryptStringFunc func(plainStr, secretKey string) (string, error) 9 | DecryptStringFunc func(encryptedStr, secretKey string) (string, error) 10 | EncryptBytesFunc func(plainBytes []byte, secretKey string) (string, error) 11 | DecryptBytesFunc func(encryptedBytes, secretKey string) ([]byte, error) 12 | HashStringFunc func(plainStr string) (string, error) 13 | GenerateRandomStringFunc func(length int) (string, error) 14 | } 15 | 16 | func (m *MockCryptographer) EncryptString(plainStr string, secretKey string) (string, error) { 17 | return m.EncryptStringFunc(plainStr, secretKey) 18 | } 19 | func (m *MockCryptographer) DecryptString(encryptedStr string, secretKey string) (string, error) { 20 | return m.DecryptStringFunc(encryptedStr, secretKey) 21 | } 22 | func (m *MockCryptographer) EncryptBytes(plainBytes []byte, secretKey string) (string, error) { 23 | return m.EncryptBytesFunc(plainBytes, secretKey) 24 | } 25 | 26 | func (m *MockCryptographer) DecryptBytes(encryptedBytes, secretKey string) ([]byte, error) { 27 | return m.DecryptBytesFunc(encryptedBytes, secretKey) 28 | } 29 | 30 | func (m *MockCryptographer) HashString(plainStr string) (string, error) { 31 | return m.HashStringFunc(plainStr) 32 | } 33 | func (m *MockCryptographer) GenerateRandomString(length int) (string, error) { 34 | return m.GenerateRandomStringFunc(length) 35 | } 36 | -------------------------------------------------------------------------------- /internal/mocks/email/mailer.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/email" 5 | "gopkg.in/gomail.v2" 6 | ) 7 | 8 | var _ domain.Mailer = (*MockGoMailer)(nil) 9 | 10 | type MockGoMailer struct { 11 | DialFunc func(host string, port int, username string, password string) (gomail.SendCloser, error) 12 | DialAndSendFunc func(host string, port int, username string, password string, message ...*gomail.Message) error 13 | NewMessageFunc func(request *domain.EmailRequest, body string, subject string, fromAddress string) *gomail.Message 14 | } 15 | 16 | func (m *MockGoMailer) Dial(host string, port int, username string, password string) (gomail.SendCloser, error) { 17 | return m.DialFunc(host, port, username, password) 18 | } 19 | 20 | func (m *MockGoMailer) DialAndSend(host string, port int, username string, password string, message ...*gomail.Message) error { 21 | return m.DialAndSendFunc(host, port, username, password, message...) 22 | } 23 | 24 | func (m *MockGoMailer) NewMessage(request *domain.EmailRequest, body string, subject string, fromAddress string) *gomail.Message { 25 | return m.NewMessageFunc(request, body, subject, fromAddress) 26 | } 27 | -------------------------------------------------------------------------------- /internal/mocks/email/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/email" 7 | ) 8 | 9 | var _ domain.EmailService = (*MockEmailService)(nil) 10 | 11 | type MockEmailService struct { 12 | SendEmailFunc func(ctx context.Context, request *domain.EmailRequest) error 13 | TestConnectionFunc func() error 14 | GetEmailRetryQueueFunc func() *domain.EmailRetryQueue 15 | } 16 | 17 | func (m *MockEmailService) SendEmail(ctx context.Context, request *domain.EmailRequest) error { 18 | return m.SendEmailFunc(ctx, request) 19 | } 20 | 21 | func (m *MockEmailService) TestConnection() error { 22 | return m.TestConnectionFunc() 23 | } 24 | 25 | func (m *MockEmailService) GetEmailRetryQueue() *domain.EmailRetryQueue { 26 | return m.GetEmailRetryQueueFunc() 27 | } 28 | -------------------------------------------------------------------------------- /internal/mocks/jwt/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | jwt "github.com/vigiloauth/vigilo/v2/internal/domain/jwt" 7 | tokens "github.com/vigiloauth/vigilo/v2/internal/domain/token" 8 | ) 9 | 10 | var _ jwt.JWTService = (*MockJWTService)(nil) 11 | 12 | type MockJWTService struct { 13 | ParseWithClaimsFunc func(ctx context.Context, tokenString string) (*tokens.TokenClaims, error) 14 | SignTokenFunc func(ctx context.Context, claims *tokens.TokenClaims) (string, error) 15 | } 16 | 17 | func (m *MockJWTService) ParseWithClaims(ctx context.Context, tokenString string) (*tokens.TokenClaims, error) { 18 | return m.ParseWithClaimsFunc(ctx, tokenString) 19 | } 20 | 21 | func (m *MockJWTService) SignToken(ctx context.Context, claims *tokens.TokenClaims) (string, error) { 22 | return m.SignTokenFunc(ctx, claims) 23 | } 24 | -------------------------------------------------------------------------------- /internal/mocks/login/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/login" 7 | user "github.com/vigiloauth/vigilo/v2/internal/domain/user" 8 | ) 9 | 10 | var _ domain.LoginAttemptRepository = (*MockLoginAttemptRepository)(nil) 11 | 12 | type MockLoginAttemptRepository struct { 13 | SaveLoginAttemptFunc func(ctx context.Context, attempt *user.UserLoginAttempt) error 14 | GetLoginAttemptsByUserIDFunc func(ctx context.Context, userID string) ([]*user.UserLoginAttempt, error) 15 | } 16 | 17 | func (m *MockLoginAttemptRepository) SaveLoginAttempt(ctx context.Context, attempt *user.UserLoginAttempt) error { 18 | return m.SaveLoginAttemptFunc(ctx, attempt) 19 | } 20 | 21 | func (m *MockLoginAttemptRepository) GetLoginAttemptsByUserID(ctx context.Context, userID string) ([]*user.UserLoginAttempt, error) { 22 | return m.GetLoginAttemptsByUserIDFunc(ctx, userID) 23 | } 24 | -------------------------------------------------------------------------------- /internal/mocks/login/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/login" 7 | user "github.com/vigiloauth/vigilo/v2/internal/domain/user" 8 | ) 9 | 10 | var _ domain.LoginAttemptService = (*MockLoginAttemptService)(nil) 11 | 12 | type MockLoginAttemptService struct { 13 | SaveLoginAttemptFunc func(ctx context.Context, attempt *user.UserLoginAttempt) error 14 | GetLoginAttemptsByUserIDFunc func(ctx context.Context, userID string) ([]*user.UserLoginAttempt, error) 15 | HandleFailedLoginAttemptFunc func(ctx context.Context, user *user.User, attempt *user.UserLoginAttempt) error 16 | } 17 | 18 | func (m *MockLoginAttemptService) SaveLoginAttempt(ctx context.Context, attempt *user.UserLoginAttempt) error { 19 | return m.SaveLoginAttemptFunc(ctx, attempt) 20 | } 21 | 22 | func (m *MockLoginAttemptService) GetLoginAttemptsByUserID(ctx context.Context, userID string) ([]*user.UserLoginAttempt, error) { 23 | return m.GetLoginAttemptsByUserIDFunc(ctx, userID) 24 | } 25 | 26 | func (m *MockLoginAttemptService) HandleFailedLoginAttempt(ctx context.Context, user *user.User, attempt *user.UserLoginAttempt) error { 27 | return m.HandleFailedLoginAttemptFunc(ctx, user, attempt) 28 | } 29 | -------------------------------------------------------------------------------- /internal/mocks/session/manager.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/session" 8 | ) 9 | 10 | var _ domain.SessionManager = (*MockSessionManager)(nil) 11 | 12 | type MockSessionManager struct { 13 | GetUserIDFromSessionFunc func(ctx context.Context, r *http.Request) (string, error) 14 | GetUserAuthenticationTimeFunc func(ctx context.Context, r *http.Request) (int64, error) 15 | } 16 | 17 | func (m *MockSessionManager) GetUserIDFromSession(ctx context.Context, r *http.Request) (string, error) { 18 | return m.GetUserIDFromSessionFunc(ctx, r) 19 | } 20 | 21 | func (m *MockSessionManager) GetUserAuthenticationTime(ctx context.Context, r *http.Request) (int64, error) { 22 | return m.GetUserAuthenticationTimeFunc(ctx, r) 23 | } 24 | -------------------------------------------------------------------------------- /internal/mocks/session/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | session "github.com/vigiloauth/vigilo/v2/internal/domain/session" 7 | ) 8 | 9 | var _ session.SessionRepository = (*MockSessionRepository)(nil) 10 | 11 | type MockSessionRepository struct { 12 | SaveSessionFunc func(ctx context.Context, sessionData *session.SessionData) error 13 | GetSessionByIDFunc func(ctx context.Context, sessionID string) (*session.SessionData, error) 14 | UpdateSessionByIDFunc func(ctx context.Context, sessionID string, sessionData *session.SessionData) error 15 | DeleteSessionByIDFunc func(ctx context.Context, sessionID string) error 16 | } 17 | 18 | func (m *MockSessionRepository) SaveSession(ctx context.Context, sessionData *session.SessionData) error { 19 | return m.SaveSessionFunc(ctx, sessionData) 20 | } 21 | 22 | func (m *MockSessionRepository) GetSessionByID(ctx context.Context, sessionID string) (*session.SessionData, error) { 23 | return m.GetSessionByIDFunc(ctx, sessionID) 24 | } 25 | 26 | func (m *MockSessionRepository) UpdateSessionByID(ctx context.Context, sessionID string, sessionData *session.SessionData) error { 27 | return m.UpdateSessionByIDFunc(ctx, sessionID, sessionData) 28 | } 29 | 30 | func (m *MockSessionRepository) DeleteSessionByID(ctx context.Context, sessionID string) error { 31 | return m.DeleteSessionByIDFunc(ctx, sessionID) 32 | } 33 | -------------------------------------------------------------------------------- /internal/mocks/session/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "net/http" 5 | 6 | session "github.com/vigiloauth/vigilo/v2/internal/domain/session" 7 | ) 8 | 9 | var _ session.SessionService = (*MockSessionService)(nil) 10 | 11 | type MockSessionService struct { 12 | CreateSessionFunc func(w http.ResponseWriter, r *http.Request, sessionData *session.SessionData) error 13 | InvalidateSessionFunc func(w http.ResponseWriter, r *http.Request) error 14 | GetUserIDFromSessionFunc func(r *http.Request) (string, error) 15 | UpdateSessionFunc func(r *http.Request, sessionData *session.SessionData) error 16 | GetSessionDataFunc func(r *http.Request) (*session.SessionData, error) 17 | } 18 | 19 | func (m *MockSessionService) CreateSession(w http.ResponseWriter, r *http.Request, sessionData *session.SessionData) error { 20 | return m.CreateSessionFunc(w, r, sessionData) 21 | } 22 | 23 | func (m *MockSessionService) InvalidateSession(w http.ResponseWriter, r *http.Request) error { 24 | return m.InvalidateSessionFunc(w, r) 25 | } 26 | 27 | func (m *MockSessionService) GetUserIDFromSession(r *http.Request) (string, error) { 28 | return m.GetUserIDFromSessionFunc(r) 29 | } 30 | 31 | func (m *MockSessionService) UpdateSession(r *http.Request, sessionData *session.SessionData) error { 32 | return m.UpdateSessionFunc(r, sessionData) 33 | } 34 | 35 | func (m *MockSessionService) GetSessionData(r *http.Request) (*session.SessionData, error) { 36 | return m.GetSessionDataFunc(r) 37 | } 38 | -------------------------------------------------------------------------------- /internal/mocks/token/creator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | claims "github.com/vigiloauth/vigilo/v2/internal/domain/claims" 8 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 9 | "github.com/vigiloauth/vigilo/v2/internal/types" 10 | ) 11 | 12 | var _ token.TokenCreator = (*MockTokenCreator)(nil) 13 | 14 | type MockTokenCreator struct { 15 | CreateAccessTokenFunc func(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string) (string, error) 16 | CreateRefreshTokenFunc func(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string) (string, error) 17 | CreateAccessTokenWithClaimsFunc func(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string, claims *claims.ClaimsRequest) (string, error) 18 | CreateIDTokenFunc func(ctx context.Context, userID string, clientID string, scopes types.Scope, nonce string, acrValues string, authTime time.Time) (string, error) 19 | } 20 | 21 | func (m *MockTokenCreator) CreateAccessToken(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string) (string, error) { 22 | return m.CreateAccessTokenFunc(ctx, subject, audience, scopes, roles, nonce) 23 | } 24 | 25 | func (m *MockTokenCreator) CreateRefreshToken(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string) (string, error) { 26 | return m.CreateRefreshTokenFunc(ctx, subject, audience, scopes, roles, nonce) 27 | } 28 | 29 | func (m *MockTokenCreator) CreateAccessTokenWithClaims(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string, claims *claims.ClaimsRequest) (string, error) { 30 | return m.CreateAccessTokenWithClaimsFunc(ctx, subject, audience, scopes, roles, nonce, claims) 31 | } 32 | 33 | func (m *MockTokenCreator) CreateIDToken(ctx context.Context, userID string, clientID string, scopes types.Scope, nonce string, acrValues string, authTime time.Time) (string, error) { 34 | return m.CreateIDTokenFunc(ctx, userID, clientID, scopes, nonce, acrValues, authTime) 35 | } 36 | -------------------------------------------------------------------------------- /internal/mocks/token/issuer.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | claims "github.com/vigiloauth/vigilo/v2/internal/domain/claims" 8 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 9 | "github.com/vigiloauth/vigilo/v2/internal/types" 10 | ) 11 | 12 | var _ token.TokenIssuer = (*MockTokenIssuer)(nil) 13 | 14 | type MockTokenIssuer struct { 15 | IssueTokenPairFunc func(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string, claims *claims.ClaimsRequest) (string, string, error) 16 | IssueIDTokenFunc func(ctx context.Context, subject string, audience string, scopes types.Scope, nonce string, acrValues string, authTime time.Time) (string, error) 17 | IssueAccessTokenFunc func(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string) (string, error) 18 | } 19 | 20 | func (m *MockTokenIssuer) IssueTokenPair(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string, claims *claims.ClaimsRequest) (string, string, error) { 21 | return m.IssueTokenPairFunc(ctx, subject, audience, scopes, roles, nonce, claims) 22 | } 23 | 24 | func (m *MockTokenIssuer) IssueIDToken(ctx context.Context, subject string, audience string, scopes types.Scope, nonce string, acrValues string, authTime time.Time) (string, error) { 25 | return m.IssueIDTokenFunc(ctx, subject, audience, scopes, nonce, acrValues, authTime) 26 | } 27 | 28 | func (m *MockTokenIssuer) IssueAccessToken(ctx context.Context, subject string, audience string, scopes types.Scope, roles string, nonce string) (string, error) { 29 | return m.IssueAccessTokenFunc(ctx, subject, audience, scopes, roles, nonce) 30 | } 31 | -------------------------------------------------------------------------------- /internal/mocks/token/manager.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | tokens "github.com/vigiloauth/vigilo/v2/internal/domain/token" 7 | ) 8 | 9 | var _ tokens.TokenManager = (*MockTokenManager)(nil) 10 | 11 | type MockTokenManager struct { 12 | IntrospectFunc func(ctx context.Context, tokenStr string) *tokens.TokenIntrospectionResponse 13 | RevokeFunc func(ctx context.Context, tokenStr string) error 14 | GetTokenDataFunc func(ctx context.Context, tokenStr string) (*tokens.TokenData, error) 15 | DeleteTokenFunc func(ctx context.Context, token string) error 16 | BlacklistTokenFunc func(ctx context.Context, token string) error 17 | DeleteExpiredTokensFunc func(ctx context.Context) error 18 | } 19 | 20 | func (m *MockTokenManager) Introspect(ctx context.Context, tokenStr string) *tokens.TokenIntrospectionResponse { 21 | return m.IntrospectFunc(ctx, tokenStr) 22 | } 23 | 24 | func (m *MockTokenManager) Revoke(ctx context.Context, tokenStr string) error { 25 | return m.RevokeFunc(ctx, tokenStr) 26 | } 27 | 28 | func (m *MockTokenManager) GetTokenData(ctx context.Context, tokenStr string) (*tokens.TokenData, error) { 29 | return m.GetTokenDataFunc(ctx, tokenStr) 30 | } 31 | 32 | func (m *MockTokenManager) DeleteToken(ctx context.Context, token string) error { 33 | return m.DeleteTokenFunc(ctx, token) 34 | } 35 | 36 | func (m *MockTokenManager) BlacklistToken(ctx context.Context, token string) error { 37 | return m.BlacklistTokenFunc(ctx, token) 38 | } 39 | 40 | func (m *MockTokenManager) DeleteExpiredTokens(ctx context.Context) error { 41 | return m.DeleteExpiredTokensFunc(ctx) 42 | } 43 | -------------------------------------------------------------------------------- /internal/mocks/token/parser.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 7 | ) 8 | 9 | var _ token.TokenParser = (*MockTokenParser)(nil) 10 | 11 | type MockTokenParser struct { 12 | ParseTokenFunc func(ctx context.Context, tokenString string) (*token.TokenClaims, error) 13 | } 14 | 15 | func (m *MockTokenParser) ParseToken(ctx context.Context, tokenStr string) (*token.TokenClaims, error) { 16 | return m.ParseTokenFunc(ctx, tokenStr) 17 | } 18 | -------------------------------------------------------------------------------- /internal/mocks/token/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 8 | ) 9 | 10 | var _ token.TokenRepository = (*MockTokenRepository)(nil) 11 | 12 | type MockTokenRepository struct { 13 | SaveTokenFunc func(ctx context.Context, token string, id string, tokenData *token.TokenData, expiration time.Time) error 14 | IsTokenBlacklistedFunc func(ctx context.Context, token string) (bool, error) 15 | GetTokenFunc func(ctx context.Context, token string) (*token.TokenData, error) 16 | DeleteTokenFunc func(ctx context.Context, token string) error 17 | BlacklistTokenFunc func(ctx context.Context, token string) error 18 | ExistsByTokenIDFunc func(ctx context.Context, tokenID string) (bool, error) 19 | GetExpiredTokensFunc func(ctx context.Context) ([]*token.TokenData, error) 20 | } 21 | 22 | func (m *MockTokenRepository) SaveToken(ctx context.Context, token string, id string, tokenData *token.TokenData, expiration time.Time) error { 23 | return m.SaveTokenFunc(ctx, token, id, tokenData, expiration) 24 | } 25 | 26 | func (m *MockTokenRepository) IsTokenBlacklisted(ctx context.Context, token string) (bool, error) { 27 | return m.IsTokenBlacklistedFunc(ctx, token) 28 | } 29 | 30 | func (m *MockTokenRepository) GetToken(ctx context.Context, token string) (*token.TokenData, error) { 31 | return m.GetTokenFunc(ctx, token) 32 | } 33 | 34 | func (m *MockTokenRepository) DeleteToken(ctx context.Context, token string) error { 35 | return m.DeleteTokenFunc(ctx, token) 36 | } 37 | 38 | func (m *MockTokenRepository) BlacklistToken(ctx context.Context, token string) error { 39 | return m.BlacklistTokenFunc(ctx, token) 40 | } 41 | 42 | func (m *MockTokenRepository) ExistsByTokenID(ctx context.Context, tokenID string) (bool, error) { 43 | return m.ExistsByTokenIDFunc(ctx, tokenID) 44 | } 45 | 46 | func (m *MockTokenRepository) GetExpiredTokens(ctx context.Context) ([]*token.TokenData, error) { 47 | return m.GetExpiredTokensFunc(ctx) 48 | } 49 | -------------------------------------------------------------------------------- /internal/mocks/token/validator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 7 | ) 8 | 9 | var _ token.TokenValidator = (*MockTokenValidator)(nil) 10 | 11 | type MockTokenValidator struct { 12 | ValidateTokenFunc func(ctx context.Context, tokenStr string) error 13 | } 14 | 15 | func (m *MockTokenValidator) ValidateToken(ctx context.Context, tokenStr string) error { 16 | return m.ValidateTokenFunc(ctx, tokenStr) 17 | } 18 | -------------------------------------------------------------------------------- /internal/mocks/user/authenticator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | users "github.com/vigiloauth/vigilo/v2/internal/domain/user" 7 | ) 8 | 9 | var _ users.UserAuthenticator = (*MockUserAuthenticator)(nil) 10 | 11 | type MockUserAuthenticator struct { 12 | AuthenticateUserFunc func(ctx context.Context, request *users.UserLoginRequest) (*users.UserLoginResponse, error) 13 | } 14 | 15 | func (m *MockUserAuthenticator) AuthenticateUser(ctx context.Context, request *users.UserLoginRequest) (*users.UserLoginResponse, error) { 16 | return m.AuthenticateUserFunc(ctx, request) 17 | } 18 | -------------------------------------------------------------------------------- /internal/mocks/user/creator.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | users "github.com/vigiloauth/vigilo/v2/internal/domain/user" 7 | ) 8 | 9 | var _ users.UserCreator = (*MockUserCreator)(nil) 10 | 11 | type MockUserCreator struct { 12 | CreateUserFunc func(ctx context.Context, user *users.User) (*users.UserRegistrationResponse, error) 13 | } 14 | 15 | func (m *MockUserCreator) CreateUser(ctx context.Context, user *users.User) (*users.UserRegistrationResponse, error) { 16 | return m.CreateUserFunc(ctx, user) 17 | } 18 | -------------------------------------------------------------------------------- /internal/mocks/user/manager.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | users "github.com/vigiloauth/vigilo/v2/internal/domain/user" 7 | ) 8 | 9 | var _ users.UserManager = (*MockUserManager)(nil) 10 | 11 | type MockUserManager struct { 12 | GetUserByUsernameFunc func(ctx context.Context, username string) (*users.User, error) 13 | GetUserByIDFunc func(ctx context.Context, userID string) (*users.User, error) 14 | DeleteUnverifiedUsersFunc func(ctx context.Context) error 15 | ResetPasswordFunc func(ctx context.Context, userEmail, newPassword, resetToken string) (*users.UserPasswordResetResponse, error) 16 | } 17 | 18 | func (m *MockUserManager) GetUserByUsername(ctx context.Context, username string) (*users.User, error) { 19 | return m.GetUserByUsernameFunc(ctx, username) 20 | } 21 | 22 | func (m *MockUserManager) GetUserByID(ctx context.Context, userID string) (*users.User, error) { 23 | return m.GetUserByIDFunc(ctx, userID) 24 | } 25 | 26 | func (m *MockUserManager) DeleteUnverifiedUsers(ctx context.Context) error { 27 | return m.DeleteUnverifiedUsersFunc(ctx) 28 | } 29 | 30 | func (m *MockUserManager) ResetPassword( 31 | ctx context.Context, 32 | userEmail string, 33 | newPassword string, 34 | resetToken string, 35 | ) (*users.UserPasswordResetResponse, error) { 36 | return m.ResetPasswordFunc(ctx, userEmail, newPassword, resetToken) 37 | } 38 | -------------------------------------------------------------------------------- /internal/mocks/user/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | user "github.com/vigiloauth/vigilo/v2/internal/domain/user" 7 | ) 8 | 9 | var _ user.UserRepository = (*MockUserRepository)(nil) 10 | 11 | type MockUserRepository struct { 12 | AddUserFunc func(ctx context.Context, user *user.User) error 13 | GetUserByIDFunc func(ctx context.Context, userID string) (*user.User, error) 14 | DeleteUserByIDFunc func(ctx context.Context, userID string) error 15 | UpdateUserFunc func(ctx context.Context, user *user.User) error 16 | GetUserByEmailFunc func(ctx context.Context, email string) (*user.User, error) 17 | GetUserByUsernameFunc func(ctx context.Context, username string) (*user.User, error) 18 | FindUnverifiedUsersOlderThanWeekFunc func(ctx context.Context) ([]*user.User, error) 19 | } 20 | 21 | func (m *MockUserRepository) AddUser(ctx context.Context, user *user.User) error { 22 | return m.AddUserFunc(ctx, user) 23 | } 24 | 25 | func (m *MockUserRepository) GetUserByID(ctx context.Context, userID string) (*user.User, error) { 26 | return m.GetUserByIDFunc(ctx, userID) 27 | } 28 | 29 | func (m *MockUserRepository) DeleteUserByID(ctx context.Context, userID string) error { 30 | return m.DeleteUserByIDFunc(ctx, userID) 31 | } 32 | 33 | func (m *MockUserRepository) UpdateUser(ctx context.Context, user *user.User) error { 34 | return m.UpdateUserFunc(ctx, user) 35 | } 36 | 37 | func (m *MockUserRepository) GetUserByEmail(ctx context.Context, email string) (*user.User, error) { 38 | return m.GetUserByEmailFunc(ctx, email) 39 | } 40 | 41 | func (m *MockUserRepository) GetUserByUsername(ctx context.Context, username string) (*user.User, error) { 42 | return m.GetUserByUsernameFunc(ctx, username) 43 | } 44 | 45 | func (m *MockUserRepository) FindUnverifiedUsersOlderThanWeek(ctx context.Context) ([]*user.User, error) { 46 | return m.FindUnverifiedUsersOlderThanWeekFunc(ctx) 47 | } 48 | -------------------------------------------------------------------------------- /internal/mocks/user/verifier.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/user" 7 | ) 8 | 9 | var _ domain.UserVerifier = (*MockUserVerifier)(nil) 10 | 11 | type MockUserVerifier struct { 12 | VerifyEmailAddressFunc func(ctx context.Context, verificationCode string) error 13 | } 14 | 15 | func (m *MockUserVerifier) VerifyEmailAddress(ctx context.Context, verificationCode string) error { 16 | return m.VerifyEmailAddressFunc(ctx, verificationCode) 17 | } 18 | -------------------------------------------------------------------------------- /internal/mocks/userconsent/repository.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/userconsent" 7 | "github.com/vigiloauth/vigilo/v2/internal/types" 8 | ) 9 | 10 | var _ domain.UserConsentRepository = (*MockUserConsentRepository)(nil) 11 | 12 | type MockUserConsentRepository struct { 13 | HasConsentFunc func(ctx context.Context, userID, clientID string, scope types.Scope) (bool, error) 14 | SaveConsentFunc func(ctx context.Context, userID, clientID string, scope types.Scope) error 15 | RevokeConsentFunc func(ctx context.Context, userID, clientID string) error 16 | } 17 | 18 | func (m *MockUserConsentRepository) HasConsent(ctx context.Context, userID, clientID string, scope types.Scope) (bool, error) { 19 | return m.HasConsentFunc(ctx, userID, clientID, scope) 20 | } 21 | 22 | func (m *MockUserConsentRepository) SaveConsent(ctx context.Context, userID, clientID string, scope types.Scope) error { 23 | return m.SaveConsentFunc(ctx, userID, clientID, scope) 24 | } 25 | 26 | func (m *MockUserConsentRepository) RevokeConsent(ctx context.Context, userID, clientID string) error { 27 | return m.RevokeConsentFunc(ctx, userID, clientID) 28 | } 29 | -------------------------------------------------------------------------------- /internal/mocks/userconsent/service.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | user "github.com/vigiloauth/vigilo/v2/internal/domain/userconsent" 8 | "github.com/vigiloauth/vigilo/v2/internal/types" 9 | ) 10 | 11 | var _ user.UserConsentService = (*MockUserConsentService)(nil) 12 | 13 | type MockUserConsentService struct { 14 | CheckUserConsentFunc func(ctx context.Context, userID, clientID string, scope types.Scope) (bool, error) 15 | SaveUserConsentFunc func(ctx context.Context, userID, clientID string, scope types.Scope) error 16 | RevokeConsentFunc func(ctx context.Context, userID, clientID string) error 17 | GetConsentDetailsFunc func(userID, clientID, redirectURI, state string, scope types.Scope, responseType, nonce, display string, r *http.Request) (*user.UserConsentResponse, error) 18 | ProcessUserConsentFunc func(userID, clientID, redirectURI string, scope types.Scope, consentRequest *user.UserConsentRequest, r *http.Request) (*user.UserConsentResponse, error) 19 | } 20 | 21 | func (m *MockUserConsentService) GetConsentDetails(userID, clientID, redirectURI, state string, scope types.Scope, responseType, nonce, display string, r *http.Request) (*user.UserConsentResponse, error) { 22 | return m.GetConsentDetailsFunc(userID, clientID, redirectURI, state, scope, responseType, nonce, display, r) 23 | } 24 | 25 | func (m *MockUserConsentService) ProcessUserConsent(userID, clientID, redirectURI string, scope types.Scope, consentRequest *user.UserConsentRequest, r *http.Request) (*user.UserConsentResponse, error) { 26 | return m.ProcessUserConsentFunc(userID, clientID, redirectURI, scope, consentRequest, r) 27 | } 28 | 29 | func (m *MockUserConsentService) CheckUserConsent(ctx context.Context, userID, clientID string, scope types.Scope) (bool, error) { 30 | return m.CheckUserConsentFunc(ctx, userID, clientID, scope) 31 | } 32 | 33 | func (m *MockUserConsentService) SaveUserConsent(ctx context.Context, userID, clientID string, scope types.Scope) error { 34 | return m.SaveUserConsentFunc(ctx, userID, clientID, scope) 35 | } 36 | 37 | func (m *MockUserConsentService) RevokeConsent(ctx context.Context, userID, clientID string) error { 38 | return m.RevokeConsentFunc(ctx, userID, clientID) 39 | } 40 | -------------------------------------------------------------------------------- /internal/repository/token/memory_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/golang-jwt/jwt" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/token" 12 | ) 13 | 14 | const ( 15 | testToken string = "test-token" 16 | testID string = "test-id" 17 | ) 18 | 19 | func TestTokenStore_AddToken(t *testing.T) { 20 | tokenStore := GetInMemoryTokenRepository() 21 | expiration := time.Now().Add(1 * time.Hour) 22 | 23 | _ = tokenStore.SaveToken(context.Background(), testToken, testID, &domain.TokenData{}, expiration) 24 | 25 | tokenStore.mu.Lock() 26 | defer tokenStore.mu.Unlock() 27 | assert.Contains(t, tokenStore.tokens, testToken) 28 | } 29 | 30 | func TestTokenStore_IsTokenBlacklisted(t *testing.T) { 31 | ctx := context.Background() 32 | tokenStore := GetInMemoryTokenRepository() 33 | expiration := time.Now().Add(-1 * time.Hour) 34 | 35 | tokenData := &domain.TokenData{ 36 | TokenClaims: &domain.TokenClaims{ 37 | StandardClaims: &jwt.StandardClaims{ 38 | ExpiresAt: expiration.Unix(), 39 | }, 40 | }, 41 | } 42 | 43 | _ = tokenStore.SaveToken(ctx, testToken, testID, tokenData, expiration) 44 | isBlacklisted, err := tokenStore.IsTokenBlacklisted(ctx, testToken) 45 | 46 | require.NoError(t, err) 47 | assert.True(t, isBlacklisted) 48 | } 49 | 50 | func TestTokenStore_DeleteToken(t *testing.T) { 51 | ctx := context.Background() 52 | tokenStore := GetInMemoryTokenRepository() 53 | expiration := time.Now().Add(1 * time.Hour) 54 | 55 | err := tokenStore.SaveToken(ctx, testToken, testID, &domain.TokenData{}, expiration) 56 | require.NoError(t, err) 57 | 58 | token, err := tokenStore.GetToken(ctx, testToken) 59 | require.NoError(t, err) 60 | assert.NotNil(t, token) 61 | 62 | err = tokenStore.DeleteToken(ctx, testToken) 63 | require.NoError(t, err) 64 | 65 | retrievedToken, err := tokenStore.GetToken(ctx, testToken) 66 | require.Error(t, err) 67 | assert.Nil(t, retrievedToken) 68 | } 69 | -------------------------------------------------------------------------------- /internal/routes/route_group.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import "net/http" 4 | 5 | type RouteGroup struct { 6 | Name string 7 | Middleware []func(http.Handler) http.Handler 8 | Routes []Route 9 | } 10 | 11 | type Route struct { 12 | Methods []string 13 | Pattern string 14 | Handler http.HandlerFunc 15 | Middleware []func(http.Handler) http.Handler 16 | Description string 17 | } 18 | 19 | type RouteBuilder struct { 20 | route Route 21 | } 22 | 23 | func NewRoute() *RouteBuilder { 24 | return &RouteBuilder{ 25 | route: Route{ 26 | Middleware: make([]func(http.Handler) http.Handler, 0), 27 | }, 28 | } 29 | } 30 | 31 | func (rb *RouteBuilder) SetMethods(methods ...string) *RouteBuilder { 32 | rb.route.Methods = methods 33 | return rb 34 | } 35 | 36 | func (rb *RouteBuilder) SetPattern(pattern string) *RouteBuilder { 37 | rb.route.Pattern = pattern 38 | return rb 39 | } 40 | 41 | func (rb *RouteBuilder) SetHandler(handler http.HandlerFunc) *RouteBuilder { 42 | rb.route.Handler = handler 43 | return rb 44 | } 45 | 46 | func (rb *RouteBuilder) SetMiddleware(middleware ...func(http.Handler) http.Handler) *RouteBuilder { 47 | rb.route.Middleware = append(rb.route.Middleware, middleware...) 48 | return rb 49 | } 50 | 51 | func (rb *RouteBuilder) SetDescription(description string) *RouteBuilder { 52 | rb.route.Description = description 53 | return rb 54 | } 55 | 56 | func (rb *RouteBuilder) Build() Route { 57 | return rb.route 58 | } 59 | 60 | func (r Route) getHTTPMethods() []string { 61 | if len(r.Methods) > 0 { 62 | return r.Methods 63 | } 64 | 65 | return []string{http.MethodGet} // Default to GET if no method is provided 66 | } 67 | -------------------------------------------------------------------------------- /internal/service/authzcode/issuer.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vigiloauth/vigilo/v2/idp/config" 7 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/authzcode" 8 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 9 | "github.com/vigiloauth/vigilo/v2/internal/errors" 10 | "github.com/vigiloauth/vigilo/v2/internal/utils" 11 | ) 12 | 13 | var _ domain.AuthorizationCodeIssuer = (*authorizationCodeIssuer)(nil) 14 | 15 | type authorizationCodeIssuer struct { 16 | creator domain.AuthorizationCodeCreator 17 | logger *config.Logger 18 | module string 19 | } 20 | 21 | func NewAuthorizationCodeIssuer( 22 | creator domain.AuthorizationCodeCreator, 23 | ) domain.AuthorizationCodeIssuer { 24 | return &authorizationCodeIssuer{ 25 | creator: creator, 26 | logger: config.GetServerConfig().Logger(), 27 | module: "Authorization Code Issuer", 28 | } 29 | } 30 | 31 | // IssueAuthorizationCode generates an authorization code for the given client request. 32 | // 33 | // Parameters: 34 | // - ctx Context: The context for managing timeouts and cancellations. 35 | // - req *ClientAuthorizationRequest: The request containing the metadata to generate an authorization code. 36 | // 37 | // Returns: 38 | // - string: The generated authorization code. 39 | // - error: An error if code generation fails. 40 | func (c *authorizationCodeIssuer) IssueAuthorizationCode( 41 | ctx context.Context, 42 | req *client.ClientAuthorizationRequest, 43 | ) (string, error) { 44 | requestID := utils.GetRequestID(ctx) 45 | 46 | code, err := c.creator.GenerateAuthorizationCode(ctx, req) 47 | if err != nil { 48 | c.logger.Error(c.module, requestID, "[IssueAuthorizationCode] Error generating authorization code: %v", err) 49 | return "", errors.Wrap(err, "", "failed to generate authorization code") 50 | } 51 | 52 | return code, nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/service/authzcode/issuer_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | client "github.com/vigiloauth/vigilo/v2/internal/domain/client" 10 | "github.com/vigiloauth/vigilo/v2/internal/errors" 11 | mocks "github.com/vigiloauth/vigilo/v2/internal/mocks/authzcode" 12 | ) 13 | 14 | func TestAuthorizationCodeIssuer_IssueAuthorizationCode(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | wantErr bool 18 | expectedErrorCode string 19 | request *client.ClientAuthorizationRequest 20 | creator *mocks.MockAuthorizationCodeCreator 21 | }{ 22 | { 23 | name: "Success", 24 | wantErr: false, 25 | expectedErrorCode: "", 26 | request: createClientAuthorizationRequest(false), 27 | creator: &mocks.MockAuthorizationCodeCreator{ 28 | GenerateAuthorizationCodeFunc: func(ctx context.Context, req *client.ClientAuthorizationRequest) (string, error) { 29 | return "test-code", nil 30 | }, 31 | }, 32 | }, 33 | { 34 | name: "Internal error generating authorization code", 35 | wantErr: true, 36 | expectedErrorCode: errors.SystemErrorCodeMap[errors.ErrCodeInternalServerError], 37 | request: createClientAuthorizationRequest(false), 38 | creator: &mocks.MockAuthorizationCodeCreator{ 39 | GenerateAuthorizationCodeFunc: func(ctx context.Context, req *client.ClientAuthorizationRequest) (string, error) { 40 | return "", errors.NewInternalServerError("") 41 | }, 42 | }, 43 | }, 44 | } 45 | 46 | for _, test := range tests { 47 | t.Run(test.name, func(t *testing.T) { 48 | ctx := context.Background() 49 | sut := NewAuthorizationCodeIssuer( 50 | test.creator, 51 | ) 52 | 53 | code, err := sut.IssueAuthorizationCode(ctx, test.request) 54 | 55 | if test.wantErr { 56 | require.Error(t, err, "Expected an error but got none") 57 | assert.Equal(t, test.expectedErrorCode, errors.SystemErrorCode(err), "Expected error code does not match") 58 | assert.Empty(t, code, "Expected empty code on error") 59 | } else { 60 | require.NoError(t, err, "Expected no error but got one") 61 | assert.NotEmpty(t, code, "Expected a non-empty code") 62 | } 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/service/crypto/cryptographer_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "github.com/vigiloauth/vigilo/v2/internal/errors" 9 | ) 10 | 11 | func TestCryptographer_HashString(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | wantErr bool 15 | expectedErrCode string 16 | plainStr string 17 | }{ 18 | { 19 | name: "Valid Hash", 20 | wantErr: false, 21 | expectedErrCode: "", 22 | plainStr: "testString", 23 | }, 24 | { 25 | name: "Empty String", 26 | wantErr: true, 27 | expectedErrCode: errors.SystemErrorCodeMap[errors.ErrCodeHashingFailed], 28 | }, 29 | } 30 | 31 | for _, test := range tests { 32 | t.Run(test.name, func(t *testing.T) { 33 | sut := NewCryptographer() 34 | hashedStr, err := sut.HashString(test.plainStr) 35 | 36 | if test.wantErr { 37 | require.Error(t, err, "Expected an error but got none") 38 | assert.Equal(t, test.expectedErrCode, errors.SystemErrorCode(err), "Expected error code does not match") 39 | assert.Empty(t, hashedStr, "Expected empty hashed string on error") 40 | } else { 41 | require.NoError(t, err, "Expected no error but got: %v", err) 42 | assert.NotEmpty(t, hashedStr, "Expected non-empty hashed string") 43 | assert.NotEqual(t, test.plainStr, hashedStr, "Hashed string should not match the plain string") 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/service/jwt/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | 7 | "github.com/golang-jwt/jwt" 8 | "github.com/vigiloauth/vigilo/v2/idp/config" 9 | domain "github.com/vigiloauth/vigilo/v2/internal/domain/jwt" 10 | tokens "github.com/vigiloauth/vigilo/v2/internal/domain/token" 11 | "github.com/vigiloauth/vigilo/v2/internal/errors" 12 | "github.com/vigiloauth/vigilo/v2/internal/utils" 13 | ) 14 | 15 | var _ domain.JWTService = (*jwtService)(nil) 16 | 17 | type jwtService struct { 18 | publicKey *rsa.PublicKey 19 | privateKey *rsa.PrivateKey 20 | keyID string 21 | 22 | logger *config.Logger 23 | module string 24 | } 25 | 26 | func NewJWTService() domain.JWTService { 27 | return &jwtService{ 28 | publicKey: config.GetServerConfig().TokenConfig().PublicKey(), 29 | privateKey: config.GetServerConfig().TokenConfig().SecretKey(), 30 | keyID: config.GetServerConfig().TokenConfig().KeyID(), 31 | logger: config.GetServerConfig().Logger(), 32 | module: "JWT Service", 33 | } 34 | } 35 | 36 | func (s *jwtService) ParseWithClaims(ctx context.Context, tokenString string) (*tokens.TokenClaims, error) { 37 | requestID := utils.GetRequestID(ctx) 38 | 39 | tokenClaims, err := jwt.ParseWithClaims(tokenString, &tokens.TokenClaims{}, func(token *jwt.Token) (any, error) { 40 | if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { 41 | s.logger.Error(s.module, requestID, "[ParseWithClaims]: Unexpected signing method received") 42 | return nil, errors.New(errors.ErrCodeTokenParsing, "unexpected signing method") 43 | } 44 | return s.publicKey, nil 45 | }) 46 | 47 | if err != nil { 48 | s.logger.Error(s.module, requestID, "[ParseWithClaims]: An error occurred parsing the token: %v", err) 49 | return nil, errors.Wrap(err, errors.ErrCodeTokenParsing, "failed to parse JWT with claims") 50 | } 51 | 52 | if claims, ok := tokenClaims.Claims.(*tokens.TokenClaims); ok && tokenClaims.Valid { 53 | return claims, nil 54 | } 55 | 56 | return nil, errors.New(errors.ErrCodeInvalidToken, "provided token is invalid") 57 | } 58 | 59 | func (s *jwtService) SignToken(ctx context.Context, claims *tokens.TokenClaims) (string, error) { 60 | jwtClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 61 | jwtClaims.Header["kid"] = s.keyID 62 | 63 | signedStr, err := jwtClaims.SignedString(s.privateKey) 64 | if err != nil { 65 | return "", errors.Wrap(err, errors.ErrCodeTokenSigning, "failed to sign token with claims") 66 | } 67 | 68 | return signedStr, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/service/token/parser.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vigiloauth/vigilo/v2/idp/config" 7 | jwt "github.com/vigiloauth/vigilo/v2/internal/domain/jwt" 8 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 9 | "github.com/vigiloauth/vigilo/v2/internal/errors" 10 | "github.com/vigiloauth/vigilo/v2/internal/utils" 11 | ) 12 | 13 | var _ token.TokenParser = (*tokenParser)(nil) 14 | 15 | type tokenParser struct { 16 | jwtService jwt.JWTService 17 | logger *config.Logger 18 | module string 19 | } 20 | 21 | func NewTokenParser(jwtService jwt.JWTService) token.TokenParser { 22 | return &tokenParser{ 23 | jwtService: jwtService, 24 | logger: config.GetServerConfig().Logger(), 25 | module: "Token Parser", 26 | } 27 | } 28 | 29 | // ParseToken parses a JWT token string into TokenClaims. 30 | // 31 | // Parameters: 32 | // - ctx ctx.Context: Context for the request, containing the request ID for logging. 33 | // - tokenString string: The JWT token string to parse and validate. 34 | // 35 | // Returns: 36 | // - *token.TokenClaims: The parsed token claims if successful. 37 | // - error: An error if token parsing, decryption, or validation fails. 38 | func (t *tokenParser) ParseToken(ctx context.Context, tokenString string) (*token.TokenClaims, error) { 39 | requestID := utils.GetRequestID(ctx) 40 | 41 | claims, err := t.jwtService.ParseWithClaims(ctx, tokenString) 42 | if err != nil { 43 | t.logger.Error(t.module, requestID, "[ParseToken]: Failed to parse token: %v", err) 44 | return nil, errors.Wrap(err, "", "failed to parse token with claims") 45 | } 46 | 47 | return claims, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/service/token/parser_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/vigiloauth/vigilo/v2/internal/constants" 10 | token "github.com/vigiloauth/vigilo/v2/internal/domain/token" 11 | "github.com/vigiloauth/vigilo/v2/internal/errors" 12 | mocks "github.com/vigiloauth/vigilo/v2/internal/mocks/jwt" 13 | ) 14 | 15 | func TestTokenParser_ParseToken(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | wantErr bool 19 | expectedErrCode string 20 | expectedClaims *token.TokenClaims 21 | jwtService *mocks.MockJWTService 22 | }{ 23 | { 24 | name: "Success", 25 | wantErr: false, 26 | expectedErrCode: "", 27 | expectedClaims: getTestTokenClaims(), 28 | jwtService: &mocks.MockJWTService{ 29 | ParseWithClaimsFunc: func(ctx context.Context, tokenString string) (*token.TokenClaims, error) { 30 | return getTestTokenClaims(), nil 31 | }, 32 | }, 33 | }, 34 | { 35 | name: "Internal server error is returned when failing to parse claims", 36 | wantErr: true, 37 | expectedErrCode: errors.SystemErrorCodeMap[errors.ErrCodeInternalServerError], 38 | expectedClaims: nil, 39 | jwtService: &mocks.MockJWTService{ 40 | ParseWithClaimsFunc: func(ctx context.Context, tokenString string) (*token.TokenClaims, error) { 41 | return nil, errors.NewInternalServerError("") 42 | }, 43 | }, 44 | }, 45 | } 46 | 47 | for _, test := range tests { 48 | t.Run(test.name, func(t *testing.T) { 49 | service := NewTokenParser(test.jwtService) 50 | ctx := context.WithValue(context.Background(), constants.ContextKeyRequestID, requestID) 51 | 52 | claims, err := service.ParseToken(ctx, "token") 53 | 54 | if test.wantErr { 55 | require.Error(t, err, "Expected an error but got none") 56 | assert.Equal(t, test.expectedErrCode, errors.SystemErrorCode(err), "Expected error codes to be equal") 57 | assert.Nil(t, claims, "Expected claims to be nil") 58 | } else { 59 | require.NoError(t, err, "Expected no error but got: %v", err) 60 | assert.NotNil(t, claims, "Expected claims to not be nil") 61 | assert.Equal(t, test.expectedClaims.Scopes, claims.Scopes, "Expected Scopes to be equal") 62 | assert.Equal(t, test.expectedClaims.Nonce, claims.Nonce, "Expected Nonce values to be equal") 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/types/client_types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ClientType string 4 | 5 | const ( 6 | ConfidentialClient ClientType = "confidential" 7 | PublicClient ClientType = "public" 8 | ) 9 | 10 | var SupportedClientTypes = map[ClientType]bool{ 11 | ConfidentialClient: true, 12 | PublicClient: true, 13 | } 14 | 15 | func (c ClientType) String() string { 16 | return string(c) 17 | } 18 | -------------------------------------------------------------------------------- /internal/types/code_challenge_methods.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type CodeChallengeMethod string 4 | 5 | const ( 6 | PlainCodeChallengeMethod CodeChallengeMethod = "plain" 7 | SHA256CodeChallengeMethod CodeChallengeMethod = "S256" 8 | ) 9 | 10 | var SupportedCodeChallengeMethods = map[CodeChallengeMethod]bool{ 11 | PlainCodeChallengeMethod: true, 12 | SHA256CodeChallengeMethod: true, 13 | } 14 | 15 | func (c CodeChallengeMethod) String() string { 16 | return string(c) 17 | } 18 | -------------------------------------------------------------------------------- /internal/types/token_endpoint_auth_types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // TokenAuthMethod represents supported OAuth 2.0 token endpoint authentication methods. 4 | // These values are typically used in the client authentication process when obtaining an access token. 5 | type TokenAuthMethod string 6 | 7 | const ( 8 | // NoTokenAuth indicates that no client authentication is used at the token endpoint. 9 | NoTokenAuth TokenAuthMethod = "none" 10 | 11 | // ClientSecretPostTokenAuth indicates client authentication using HTTP POST parameters (client_id and client_secret in the body). 12 | ClientSecretPostTokenAuth TokenAuthMethod = "client_secret_post" 13 | 14 | // ClientSecretBasicTokenAuth indicates client authentication using HTTP Basic Authentication (client_id and client_secret in the Authorization header). 15 | ClientSecretBasicTokenAuth TokenAuthMethod = "client_secret_basic" 16 | ) 17 | 18 | // SupportedTokenEndpointAuthMethods defines the set of supported and recognized token endpoint authentication methods. 19 | // This can be used for validating incoming configuration or requests. 20 | var SupportedTokenEndpointAuthMethods = map[TokenAuthMethod]bool{ 21 | NoTokenAuth: true, 22 | ClientSecretBasicTokenAuth: true, 23 | ClientSecretPostTokenAuth: true, 24 | } 25 | 26 | func (t TokenAuthMethod) String() string { 27 | return string(t) 28 | } 29 | -------------------------------------------------------------------------------- /internal/types/token_types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type TokenType string 4 | 5 | const ( 6 | RefreshTokenType TokenType = "refresh" 7 | AccessTokenType TokenType = "access" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/utils/context.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/vigiloauth/vigilo/v2/internal/constants" 7 | ) 8 | 9 | // GetRequestID retrieves the request ID from the context. 10 | // 11 | // Parameters: 12 | // - ctx context.Context: The context containing the request ID. 13 | // 14 | // Returns: 15 | // - string: The request ID as a string if present, otherwise an empty string. 16 | func GetRequestID(ctx context.Context) string { 17 | if requestID, ok := ctx.Value(constants.ContextKeyRequestID).(string); ok { 18 | return requestID 19 | } 20 | 21 | return "" 22 | } 23 | 24 | // GetValueFromContext retrieves a string value from the context based on the provided key. 25 | // 26 | // Parameters: 27 | // - ctx ctx.Context: The context from which to retrieve the value. 28 | // - value constants.ContextKey: The key of type constants.ContextKey used to retrieve the value. 29 | // 30 | // Returns: 31 | // - any: The value if found, otherwise an empty string. 32 | func GetValueFromContext(ctx context.Context, value constants.ContextKey) any { 33 | return ctx.Value(value) 34 | } 35 | 36 | // AddKeyValueToContext returns a new context with the specified key-value pair added. 37 | // 38 | // Parameters: 39 | // - ctx context.Context: The base context. 40 | // - key constants.ContextKey: The key of type constants.ContextKey to associate with the value. 41 | // - value any: The value to store in the context. 42 | // 43 | // Returns: 44 | // - context.Context: A new context with the key-value pair added. 45 | func AddKeyValueToContext(ctx context.Context, key constants.ContextKey, value any) context.Context { 46 | return context.WithValue(ctx, key, value) 47 | } 48 | -------------------------------------------------------------------------------- /internal/utils/logging.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // TruncateSensitive shortens sensitive strings for safe logging. 8 | // 9 | // Parameters: 10 | // - data string: The sensitive string to truncate. 11 | // 12 | // Returns: 13 | // - string: A truncated version of the string with "[REDACTED]" appended if its length 14 | // is greater than 5. Otherwise, returns the original string. 15 | func TruncateSensitive(data string) string { 16 | const minDataLength int = 5 17 | if len(data) > minDataLength { 18 | return data[:minDataLength] + "[REDACTED]" 19 | } 20 | 21 | return data 22 | } 23 | 24 | // SanitizeURL redacts query parameters from the provided URL for secure logging. 25 | // 26 | // Parameters: 27 | // - uri string: The URL string to sanitize. 28 | // 29 | // Returns: 30 | // - string: The sanitized URL with query parameters replaced by "[REDACTED]". 31 | // If the URL is invalid, returns "[INVALID URL]". 32 | func SanitizeURL(uri string) string { 33 | parsed, err := url.Parse(uri) 34 | if err != nil { 35 | return "[INVALID URL]" 36 | } 37 | 38 | parsed.RawQuery = "[REDACTED]" 39 | return parsed.String() 40 | } 41 | -------------------------------------------------------------------------------- /internal/web/decode_json.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/vigiloauth/vigilo/v2/internal/errors" 8 | ) 9 | 10 | // DecodeJSONRequest decodes the JSON request body into the provided generic type T. 11 | // It reads the request body, attempts to decode it into the specified type, and returns 12 | // a pointer to the decoded object. If decoding fails, it returns an error wrapped with 13 | // an internal server error code. 14 | // 15 | // Parameters: 16 | // 17 | // - w: The HTTP response writer. 18 | // - r: The HTTP request containing the JSON body to decode. 19 | // 20 | // Returns: 21 | // 22 | // - A pointer to the decoded object of type T, or an error if decoding fails. 23 | func DecodeJSONRequest[T any](w http.ResponseWriter, r *http.Request) (*T, error) { 24 | var request T 25 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { 26 | return nil, errors.Wrap(err, errors.ErrCodeInternalServerError, "failed to decode request") 27 | } 28 | 29 | return &request, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/web/endpoints.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | const ( 4 | defaultAuthEndpoint string = "/auth" 5 | defaultClientEndpoint string = "/clients" 6 | defaultOAuthEndpoint string = "/oauth2" 7 | defaultTokenEndpoint string = "/tokens" 8 | defaultAdminEndpoint string = "/admins" 9 | wellKnown string = "/.well-known" 10 | ) 11 | 12 | var UserEndpoints = struct { 13 | Registration string 14 | Login string 15 | Logout string 16 | ResetPassword string 17 | Verify string 18 | }{ 19 | Registration: defaultAuthEndpoint + "/signup", 20 | Login: defaultAuthEndpoint + "/login", 21 | Logout: defaultAuthEndpoint + "/logout", 22 | ResetPassword: defaultAuthEndpoint + "/reset-password/confirm", 23 | Verify: defaultAuthEndpoint + "/verify", 24 | } 25 | 26 | var ClientEndpoints = struct { 27 | RegenerateSecret string 28 | ClientConfiguration string 29 | Register string 30 | GetClientByID string 31 | }{ 32 | Register: defaultClientEndpoint + "/register", 33 | RegenerateSecret: defaultClientEndpoint + "/regenerate-secret", 34 | ClientConfiguration: defaultOAuthEndpoint + defaultClientEndpoint + "/register", 35 | GetClientByID: defaultOAuthEndpoint + defaultClientEndpoint, 36 | } 37 | 38 | var OAuthEndpoints = struct { 39 | Token string 40 | Authorize string 41 | Authenticate string 42 | UserConsent string 43 | TokenExchange string 44 | IntrospectToken string 45 | RevokeToken string 46 | }{ 47 | Token: defaultOAuthEndpoint + "/token", 48 | Authorize: defaultOAuthEndpoint + "/authorize", 49 | Authenticate: defaultOAuthEndpoint + "/authenticate", 50 | UserConsent: defaultOAuthEndpoint + "/consent", 51 | TokenExchange: defaultOAuthEndpoint + "/token", 52 | IntrospectToken: defaultOAuthEndpoint + defaultTokenEndpoint + "/introspect", 53 | RevokeToken: defaultOAuthEndpoint + defaultTokenEndpoint + "/revoke", 54 | } 55 | 56 | var AdminEndpoints = struct { 57 | GetAuditEvents string 58 | }{ 59 | GetAuditEvents: defaultAdminEndpoint + "/audit-events", 60 | } 61 | 62 | var OIDCEndpoints = struct { 63 | UserInfo string 64 | JWKS string 65 | Discovery string 66 | }{ 67 | UserInfo: defaultOAuthEndpoint + "/userinfo", 68 | JWKS: defaultOAuthEndpoint + wellKnown + "/jwks.json", 69 | Discovery: defaultOAuthEndpoint + wellKnown + "/openid-configuration", 70 | } 71 | -------------------------------------------------------------------------------- /scripts/push_docker_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script builds the Docker image and pushes it to the repository. 4 | 5 | IMAGE_NAME="vigilo-auth" 6 | TAG_NAME="latest-dev" 7 | DOCKER_REPO="vigiloauth" 8 | 9 | FULL_IMAGE="$DOCKER_REPO/$IMAGE_NAME:$TAG_NAME" 10 | 11 | # Build the Docker image 12 | echo "Building Docker image..." 13 | cd .. 14 | docker build \ 15 | --cache-from $FULL_IMAGE \ 16 | --build-arg BUILDKIT_INLINE_CACHE=1 \ 17 | -t $IMAGE_NAME:$TAG_NAME . 18 | 19 | # Tag the image for the repository 20 | echo "Tagging Docker image as $FULL_IMAGE..." 21 | docker tag $IMAGE_NAME:$TAG_NAME $FULL_IMAGE 22 | 23 | # Login to Docker 24 | echo "Logging in to Docker..." 25 | docker login 26 | 27 | # Push the image 28 | echo "Pushing Docker image to repository..." 29 | docker push $FULL_IMAGE 30 | -------------------------------------------------------------------------------- /scripts/run_docker_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script creates a new docker container 4 | # for local/manual testing purposes. 5 | 6 | set -e # Exit on any error. 7 | 8 | CONTAINER_NAME="vigilo-auth-local" 9 | IMAGE_NAME="vigilo-auth" 10 | TAG_NAME="local" 11 | CONTAINER_PORT=8080 12 | 13 | # Stop all running containers 14 | echo "Stopping all running Docker containers..." 15 | docker stop $(docker ps -q) 2>/dev/null || true 16 | 17 | # Remove all containers (not just running ones) 18 | echo "Removing all Docker containers..." 19 | docker rm $(docker ps -a -q) 2>/dev/null || true 20 | 21 | # Build the Docker image 22 | echo "Building Docker image..." 23 | cd .. 24 | docker build -t $IMAGE_NAME:$TAG_NAME . 25 | 26 | # Run the container 27 | echo "Starting local container..." 28 | docker run -p $CONTAINER_PORT:$CONTAINER_PORT --name $CONTAINER_NAME \ 29 | --env-file .env \ 30 | $IMAGE_NAME:$TAG_NAME -------------------------------------------------------------------------------- /scripts/update_imports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script updates all Go import paths in the project to reflect a new major version. 4 | # Specifically, it replaces occurrences of "github.com/vigiloauth/vigilo/{CURRENT_VERSION}" with 5 | # "github.com/vigiloauth/vigilo/{NEW_VERSION}" in all .go files within the project. 6 | 7 | CURRENT_VERSION=v2 8 | SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 9 | PROJECT_ROOT="$SCRIPT_DIR/.." 10 | IMPORT_PATH=github.com/vigiloauth/vigilo 11 | 12 | while true; do 13 | read -p "Enter the new version (e.g., 4 for v4): " NEW_VERSION 14 | 15 | if [[ -z "$NEW_VERSION" ]]; then 16 | echo -e "Error: Version cannot be empty. Please try again.\n\n" 17 | continue 18 | fi 19 | 20 | if ! [[ "$NEW_VERSION" =~ ^[0-9]+$ ]]; then 21 | echo -e "Error: Version must be a number. Please try again.\n\n" 22 | continue 23 | fi 24 | 25 | NEW_VERSION="v$NEW_VERSION" 26 | break 27 | done 28 | 29 | # Navigate to the root of the project 30 | cd "$PROJECT_ROOT" || exit 1 31 | 32 | # Find all .go files and update imports 33 | find "$PROJECT_ROOT" \ 34 | -type f \ 35 | -name "*.go" \ 36 | -exec sed -i '' "s|$IMPORT_PATH/$CURRENT_VERSION|$IMPORT_PATH/$NEW_VERSION|g" {} + 37 | 38 | echo "Import paths updated from $CURRENT_VERSION to $NEW_VERSION successfully." -------------------------------------------------------------------------------- /tests/integration/admin_handler_integration_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/vigiloauth/vigilo/v2/internal/constants" 11 | "github.com/vigiloauth/vigilo/v2/internal/web" 12 | ) 13 | 14 | func TestAdminHandler_GetAuditEvents(t *testing.T) { 15 | t.Run("Success", func(t *testing.T) { 16 | testContext := NewVigiloTestContext(t) 17 | defer testContext.TearDown() 18 | 19 | testContext.WithAdminToken(testUserID, time.Duration(2*time.Hour)) 20 | testContext.WithAuditEvents() 21 | headers := map[string]string{"Authorization": constants.BearerAuthHeader + testContext.JWTToken} 22 | 23 | from := time.Now().Add(-5 * time.Hour).UTC().Format(time.RFC3339) 24 | to := time.Now().UTC().Format(time.RFC3339) 25 | 26 | queryParams := url.Values{} 27 | queryParams.Add("from", from) 28 | queryParams.Add("to", to) 29 | queryParams.Add("UserID", testUserID) 30 | queryParams.Add("EventType", "login_attempt") 31 | queryParams.Add("Success", "false") 32 | queryParams.Add("IP", testIP) 33 | queryParams.Add("limit", "50") 34 | queryParams.Add("offset", "0") 35 | endpoint := web.AdminEndpoints.GetAuditEvents + "?" + queryParams.Encode() 36 | 37 | rr := testContext.SendHTTPRequest(http.MethodGet, endpoint, nil, headers) 38 | 39 | assert.Equal(t, http.StatusOK, rr.Code) 40 | }) 41 | 42 | t.Run("Error is returned when user does not have the required roles", func(t *testing.T) { 43 | testContext := NewVigiloTestContext(t) 44 | defer testContext.TearDown() 45 | 46 | testContext.WithUser([]string{constants.UserRole}) 47 | testContext.WithJWTToken(testUserID, time.Duration(2*time.Hour)) 48 | testContext.WithAuditEvents() 49 | headers := map[string]string{"Authorization": constants.BearerAuthHeader + testContext.JWTToken} 50 | 51 | from := time.Now().Add(-5 * time.Hour).UTC().Format(time.RFC3339) 52 | to := time.Now().UTC().Format(time.RFC3339) 53 | 54 | queryParams := url.Values{} 55 | queryParams.Add("from", from) 56 | queryParams.Add("to", to) 57 | queryParams.Add("UserID", testUserID) 58 | queryParams.Add("EventType", "login_attempt") 59 | queryParams.Add("Success", "false") 60 | queryParams.Add("IP", testIP) 61 | queryParams.Add("limit", "50") 62 | queryParams.Add("offset", "0") 63 | endpoint := web.AdminEndpoints.GetAuditEvents + "?" + queryParams.Encode() 64 | 65 | rr := testContext.SendHTTPRequest(http.MethodGet, endpoint, nil, headers) 66 | 67 | assert.Equal(t, http.StatusForbidden, rr.Code) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /tests/integration/rate_limiting_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "github.com/vigiloauth/vigilo/v2/idp/config" 12 | "github.com/vigiloauth/vigilo/v2/internal/constants" 13 | users "github.com/vigiloauth/vigilo/v2/internal/domain/user" 14 | "github.com/vigiloauth/vigilo/v2/internal/web" 15 | ) 16 | 17 | func TestRateLimiting(t *testing.T) { 18 | testContext := NewVigiloTestContext(t) 19 | defer testContext.TearDown() 20 | 21 | user := users.NewUser("", testEmail, testPassword1) 22 | user.ID = testUserID 23 | 24 | request := users.UserLoginRequest{Username: testUsername, Password: testPassword1} 25 | requestBody, err := json.Marshal(request) 26 | require.NoError(t, err, "failed to marshal request body") 27 | 28 | requestsPerMinute := 5 29 | testContext.WithCustomConfig(config.WithMaxRequestsPerMinute(requestsPerMinute)) 30 | 31 | for range requestsPerMinute { 32 | testContext.WithUser([]string{constants.AdminRole}) 33 | rr := testContext.SendHTTPRequest( 34 | http.MethodPost, 35 | web.UserEndpoints.Login, 36 | bytes.NewBuffer(requestBody), 37 | nil, 38 | ) 39 | 40 | assert.Equal(t, http.StatusOK, rr.Code) 41 | testContext.ClearSession() 42 | } 43 | 44 | rr := testContext.SendHTTPRequest( 45 | http.MethodPost, 46 | web.UserEndpoints.Login, 47 | bytes.NewBuffer(requestBody), 48 | nil, 49 | ) 50 | 51 | assert.Equal(t, http.StatusOK, rr.Code) 52 | } 53 | -------------------------------------------------------------------------------- /ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import pluginReact from "eslint-plugin-react"; 4 | import { defineConfig } from "eslint/config"; 5 | 6 | 7 | export default defineConfig([ 8 | { files: ["**/*.{js,mjs,cjs,jsx}"], plugins: { js }, extends: ["js/recommended"] }, 9 | { files: ["**/*.{js,mjs,cjs,jsx}"], languageOptions: { globals: globals.browser } }, 10 | pluginReact.configs.flat.recommended, 11 | ]); -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-server-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^6.0.0", 7 | "antd": "^5.25.1", 8 | "lucide-react": "^0.510.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-router-dom": "^7.5.3", 12 | "react-scripts": "5.0.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject", 19 | "lint": "eslint ." 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@eslint/js": "^9.26.0", 40 | "@types/react": "^19.1.3", 41 | "@types/react-dom": "^19.1.3", 42 | "autoprefixer": "^10.4.14", 43 | "eslint": "^8.57.1", 44 | "eslint-config-airbnb": "^19.0.4", 45 | "eslint-config-prettier": "^10.1.3", 46 | "eslint-plugin-jsx-a11y": "^6.10.2", 47 | "eslint-plugin-prettier": "^5.4.0", 48 | "eslint-plugin-react": "^7.37.5", 49 | "globals": "^16.1.0", 50 | "postcss": "^8.4.27", 51 | "prettier": "^3.5.3", 52 | "prettier-eslint": "^16.4.2", 53 | "sass": "^1.87.0", 54 | "sass-loader": "^16.0.5", 55 | "tailwindcss": "^3.3.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | VigiloAuth 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /ui/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import "./index.css"; 3 | import AuthenticationPage from "./pages/AuthenticationPage"; 4 | import ConsentPage from "./pages/ConsentPage"; 5 | import { ApplicationContextProvider } from "./context/ApplicationContext"; 6 | import URL_PARAMS from "./constants/urlParams"; 7 | import ErrorPage from "./pages/ErrorPage"; 8 | 9 | function App() { 10 | const queryParams = new URLSearchParams(window.location.search); 11 | const display = queryParams.get(URL_PARAMS.DISPLAY); 12 | const errorType = queryParams.get(URL_PARAMS.TYPE); 13 | 14 | return ( 15 | 16 | 17 |
18 | 19 | } 22 | /> 23 | } 26 | /> 27 | } 30 | /> 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /ui/src/api/clientApi.js: -------------------------------------------------------------------------------- 1 | import ENDPOINT from "../constants/endpoints"; 2 | 3 | export async function getClientByID({ clientID }) { 4 | const endpoint = `${ENDPOINT.GET_CLIENT_BY_ID}/${clientID}`; 5 | try { 6 | const response = await fetch(endpoint, { 7 | method: "GET", 8 | }); 9 | return await response.json(); 10 | } catch (err) { 11 | throw new Error("Something went wrong retrieving the client's logo."); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/components/BaseButton.jsx: -------------------------------------------------------------------------------- 1 | import { Form, Button } from "antd"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | const BaseButton = ({ 6 | type = "primary", 7 | onClick, 8 | loading, 9 | children, 10 | style = {}, 11 | block = true, 12 | disabled = false, 13 | size = "default", 14 | }) => { 15 | return ( 16 | 17 | 28 | 29 | ); 30 | }; 31 | 32 | BaseButton.propTypes = { 33 | type: PropTypes.oneOf(["primary", "link", "text", "default", "dashed"]), 34 | onClick: PropTypes.func, 35 | loading: PropTypes.bool, 36 | children: PropTypes.node.isRequired, 37 | style: PropTypes.object, 38 | block: PropTypes.bool, 39 | disabled: PropTypes.bool, 40 | size: PropTypes.oneOf(["large", "default", "small"]), 41 | }; 42 | 43 | export default BaseButton; 44 | -------------------------------------------------------------------------------- /ui/src/components/Container.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | /** 5 | * Container 6 | * 7 | * A generic container component that applies customizable styles, 8 | * including width and maxWidth, to its children. 9 | * 10 | * @component 11 | * @example 12 | * 13 | *

Hello World

14 | *
15 | * 16 | * @param {object} props - The props for the component. 17 | * @param {React.ReactNode} props.children - The content to render inside the container. 18 | * @param {string} [props.width="100%"] - CSS width of the container. 19 | * @param {string} [props.maxWidth="350px"] - CSS maxWidth of the container. 20 | * @param {object} [props.style={}] - Additional CSS styles to apply. 21 | * 22 | * @returns {JSX.Element} The styled container element. 23 | */ 24 | const Container = ({ 25 | children, 26 | width = "100%", 27 | maxWidth = "350px", 28 | maxHeight = "600px", 29 | style = {}, 30 | layout = "vertical", 31 | className, 32 | }) => { 33 | return ( 34 |
44 | {children} 45 |
46 | ); 47 | }; 48 | 49 | Container.propTypes = { 50 | children: PropTypes.node.isRequired, 51 | width: PropTypes.string, 52 | maxWidth: PropTypes.string, 53 | style: PropTypes.object, 54 | layout: PropTypes.string, 55 | className: PropTypes.string, 56 | }; 57 | 58 | export default Container; 59 | -------------------------------------------------------------------------------- /ui/src/components/FlexContainer.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { Flex } from "antd"; 3 | 4 | const FlexContainer = ({ 5 | children, 6 | vertical = false, 7 | justify = "center", 8 | align = "center", 9 | width, 10 | height, 11 | className, 12 | style = {}, 13 | }) => { 14 | return ( 15 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | FlexContainer.propTypes = { 28 | children: PropTypes.node.isRequired, 29 | vertical: PropTypes.bool, 30 | justify: PropTypes.oneOf([ 31 | "start", 32 | "center", 33 | "end", 34 | "space-around", 35 | "space-between", 36 | "space-evenly", 37 | ]), 38 | align: PropTypes.oneOf(["start", "center", "end", "stretch", "baseline"]), 39 | style: PropTypes.object, 40 | height: PropTypes.string, 41 | className: PropTypes.string, 42 | }; 43 | 44 | export default FlexContainer; 45 | -------------------------------------------------------------------------------- /ui/src/components/FormInput.jsx: -------------------------------------------------------------------------------- 1 | import { Form, Input } from "antd"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | /** 6 | * A reusable form input component that supports both text and password inputs. 7 | * 8 | * @component 9 | * @param {Object} props - The props object. 10 | * @param {string} props.label - The label for the form input. 11 | * @param {string} props.name - The name of the form field (used for form data binding). 12 | * @param {boolean} props.required - Whether the field is required. 13 | * @param {string} props.message - Validation message shown when the field is invalid. 14 | * @param {string} props.value - The current value of the input. 15 | * @param {function} props.onChange - Callback function triggered when the input value changes. 16 | * @param {React.ReactNode} props.icon - Optional icon to display in the input prefix. 17 | * @param {boolean} [props.isPassword=false] - If true, renders a password input field. 18 | * 19 | * @returns {JSX.Element} A Form.Item with an Input or Password Input inside. 20 | */ 21 | const FormInput = ({ 22 | label, 23 | placeholder, 24 | name, 25 | required, 26 | message, 27 | value, 28 | onChange, 29 | icon, 30 | isPassword = false, 31 | validateStatus = "", 32 | size = "middle", 33 | style = {}, 34 | className, 35 | ...props 36 | }) => { 37 | const InputComponent = isPassword ? Input.Password : Input; 38 | return ( 39 | 46 | 55 | 56 | ); 57 | }; 58 | 59 | FormInput.propTypes = { 60 | label: PropTypes.string, 61 | name: PropTypes.string.isRequired, 62 | required: PropTypes.bool, 63 | message: PropTypes.string, 64 | value: PropTypes.string, 65 | onChange: PropTypes.func, 66 | icon: PropTypes.node, 67 | isPassword: PropTypes.bool, 68 | validateStatus: PropTypes.string, 69 | placeholder: PropTypes.string, 70 | size: PropTypes.oneOf(["large", "middle", "small"]), 71 | className: PropTypes.string, 72 | }; 73 | 74 | export default FormInput; 75 | -------------------------------------------------------------------------------- /ui/src/constants/endpoints.js: -------------------------------------------------------------------------------- 1 | const ENDPOINT = { 2 | USER_AUTH: "/identity/oauth2/authenticate", 3 | USER_CONSENT: "/identity/oauth2/consent", 4 | GET_CLIENT_BY_ID: "/identity/oauth2/clients", 5 | }; 6 | 7 | export default ENDPOINT; 8 | -------------------------------------------------------------------------------- /ui/src/constants/urlParams.js: -------------------------------------------------------------------------------- 1 | const URL_PARAMS = { 2 | CLIENT_ID: "client_id", 3 | REDIRECT_URI: "redirect_uri", 4 | STATE: "state", 5 | SCOPE: "scope", 6 | RESPONSE_TYPE: "response_type", 7 | NONCE: "nonce", 8 | DISPLAY: "display", 9 | TYPE: "type", 10 | ACR: "acr_values", 11 | CLAIMS: "claims", 12 | }; 13 | 14 | export default URL_PARAMS; 15 | -------------------------------------------------------------------------------- /ui/src/context/ApplicationContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect } from "react"; 2 | import { getClientByID } from "../api/clientApi"; 3 | 4 | import URL_PARAMS from "../constants/urlParams"; 5 | 6 | const ApplicationContext = createContext(null); 7 | 8 | export const ApplicationContextProvider = ({ children }) => { 9 | const queryParams = new URLSearchParams(window.location.search); 10 | 11 | const [queryValues, setQueryValues] = useState({ 12 | clientID: queryParams.get(URL_PARAMS.CLIENT_ID), 13 | redirectURI: queryParams.get(URL_PARAMS.REDIRECT_URI), 14 | state: queryParams.get(URL_PARAMS.STATE) || "", 15 | scope: queryParams.get(URL_PARAMS.SCOPE) || "", 16 | responseType: queryParams.get(URL_PARAMS.RESPONSE_TYPE) || "", 17 | nonce: queryParams.get(URL_PARAMS.NONCE) || "", 18 | display: queryParams.get(URL_PARAMS.DISPLAY) || "", 19 | acrValues: queryParams.get(URL_PARAMS.ACR) || "", 20 | claims: queryParams.get(URL_PARAMS.CLAIMS) || "", 21 | }); 22 | 23 | const [clientInfo, setClientInfo] = useState(null); 24 | const [loading, setLoading] = useState(true); 25 | 26 | useEffect(() => { 27 | if (!queryValues.clientID) return; 28 | const fetchClientInfo = async () => { 29 | // try { 30 | // setClientInfo({ 31 | // logo_uri: "https://www.certification.openid.net/images/openid.png", 32 | // policy_uri: "https://www.certification.openid.net/login.html", 33 | // }); 34 | // } catch (err) { 35 | // console.log(err); 36 | // } finally { 37 | // setLoading(false); 38 | // } 39 | try { 40 | const data = await getClientByID({ clientID: queryValues.clientID }); 41 | setClientInfo(data); 42 | } catch (err) { 43 | console.error("Error fetching client info:", err); 44 | } finally { 45 | setLoading(false); 46 | } 47 | }; 48 | 49 | fetchClientInfo(); 50 | }, [queryValues.clientID]); 51 | 52 | return ( 53 | 56 | {children} 57 | 58 | ); 59 | }; 60 | 61 | export const useApplicationContext = () => useContext(ApplicationContext); 62 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background-color: #f5f5f5; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /ui/src/pages/AuthenticationPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Card, Typography, Spin } from "antd"; 3 | import FlexContainer from "../components/FlexContainer"; 4 | import Container from "../components/Container"; 5 | import AuthenticationForm from "../forms/AuthenticationForm"; 6 | import AuthenticationPopup from "../popups/AuthenticationPopup"; 7 | import { useApplicationContext } from "../context/ApplicationContext"; 8 | import "../styles/AuthenticationPage.scss"; 9 | 10 | const { Title, Text } = Typography; 11 | 12 | export default function AuthenticationPage({ display }) { 13 | const [clientLogo, setClientLogo] = useState(null); 14 | const [clientName, setClientName] = useState(""); 15 | const [imageLoaded, setImageLoaded] = useState(false); 16 | const { clientInfo, loading: contextLoading } = useApplicationContext(); 17 | 18 | const defaultSignInMessage = "Please sign in to continue"; 19 | const signInMessage = `You are signing into ${clientName}`; 20 | 21 | useEffect(() => { 22 | if (!clientInfo) return; 23 | setClientName(clientInfo.name || ""); 24 | 25 | if (!clientInfo.logo_uri) return; 26 | const img = new Image(); 27 | img.src = clientInfo.logo_uri; 28 | img.onload = () => { 29 | setClientLogo(client logo); 30 | setImageLoaded(true); 31 | }; 32 | img.onerror = () => { 33 | setClientLogo(null); 34 | setImageLoaded(true); 35 | }; 36 | }, [clientInfo]); 37 | 38 | const isLoading = contextLoading || (clientInfo?.logo_uri && !imageLoaded); 39 | 40 | if (isLoading) { 41 | return ( 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | return display === "popup" ? ( 49 | 50 | ) : ( 51 | 52 | 53 | 54 | {clientLogo ? clientLogo : Welcome Back} 55 | 56 | {clientName ? signInMessage : defaultSignInMessage} 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /ui/src/pages/ConsentPage.jsx: -------------------------------------------------------------------------------- 1 | import { Card } from "antd"; 2 | import FlexContainer from "../components/FlexContainer"; 3 | import ConsentForm from "../forms/ConsentForm"; 4 | import ConsentPopup from "../popups/ConsentPopup"; 5 | import "../styles/ConsentPage.scss"; 6 | import { useApplicationContext } from "../context/ApplicationContext"; 7 | import { useEffect, useState } from "react"; 8 | 9 | export default function ConsentPage({ display }) { 10 | const [policyURI, setPolicyURI] = useState(""); 11 | const { clientInfo } = useApplicationContext(); 12 | 13 | useEffect(() => { 14 | if (!clientInfo || !clientInfo.policy_uri) return; 15 | setPolicyURI(clientInfo.policy_uri); 16 | }, [clientInfo]); 17 | 18 | return display === "popup" ? ( 19 | 20 | ) : ( 21 | 22 | 23 | {} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/pages/ErrorPage.jsx: -------------------------------------------------------------------------------- 1 | import InvalidRedirectErrorPage from "../forms/InvalidRedirectErrorPage"; 2 | 3 | export default function ErrorPage({ errorType }) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/popups/AuthenticationPopup.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Modal, Typography, Spin } from "antd"; 3 | import { useApplicationContext } from "../context/ApplicationContext"; 4 | import Container from "../components/Container"; 5 | import AuthenticationForm from "../forms/AuthenticationForm"; 6 | import FlexContainer from "../components/FlexContainer"; 7 | import "../styles/popup.scss"; 8 | 9 | const { Title, Text } = Typography; 10 | 11 | export default function AuthenticationPopup({ 12 | clientLogo, 13 | clientName, 14 | policyURI, 15 | loading, 16 | }) { 17 | const [visible, setVisible] = useState(true); 18 | const { redirectURI } = useApplicationContext(); 19 | 20 | const onCancel = () => { 21 | setVisible(false); 22 | }; 23 | 24 | const afterClose = () => { 25 | if (redirectURI) { 26 | window.location.replace(redirectURI); 27 | } 28 | }; 29 | 30 | const defaultSignInMessage = "Please sign in to continue"; 31 | const signInMessage = `You are signing into ${clientName}`; 32 | 33 | if (loading) { 34 | return ( 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | return ( 42 | 52 | 53 | {clientLogo ? clientLogo : Welcome Back} 54 | 55 | {clientName ? signInMessage : defaultSignInMessage} 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /ui/src/popups/ConsentPopup.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Modal } from "antd"; 3 | import { useApplicationContext } from "../context/ApplicationContext"; 4 | import ConsentForm from "../forms/ConsentForm"; 5 | import "../styles/popup.scss"; 6 | 7 | export default function ConsentPopup({ policyURI }) { 8 | const [visible, setVisible] = useState(true); 9 | const { redirectURI } = useApplicationContext(); 10 | 11 | const onCancel = () => { 12 | setVisible(false); 13 | }; 14 | 15 | const afterClose = () => { 16 | if (redirectURI) { 17 | window.location.replace(redirectURI); 18 | } 19 | }; 20 | 21 | return ( 22 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/styles/AuthenticationPage.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #1890ff; 2 | $secondary-color: #001529; 3 | $text-color: rgba(0, 0, 0, 0.85); 4 | $text-color-secondary: rgba(0, 0, 0, 0.45); 5 | $border-radius: 8px; 6 | $box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 7 | $font-family: 8 | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", 9 | Arial, sans-serif; 10 | $mobile-breakpoint: 768px; 11 | 12 | .auth-container { 13 | overflow: hidden; 14 | height: 100vh; 15 | background-color: #f5f5f5; 16 | padding: 40px 16px; 17 | @media (max-width: $mobile-breakpoint) { 18 | padding: 0; 19 | } 20 | } 21 | 22 | .auth-card { 23 | box-shadow: $box-shadow; 24 | border-radius: $border-radius; 25 | width: 100%; 26 | max-width: 450px; 27 | min-height: auto; 28 | padding: 32px; 29 | background-color: #fff; 30 | 31 | @media (max-width: $mobile-breakpoint) { 32 | border-radius: 0; 33 | box-shadow: none; 34 | max-width: 100%; 35 | min-height: 100vh; 36 | margin: 0; 37 | padding: 24px 16px; 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: center; 41 | } 42 | } 43 | 44 | .auth-header { 45 | text-align: center; 46 | margin-bottom: 32px; 47 | user-select: none; 48 | 49 | @media (max-width: $mobile-breakpoint) { 50 | margin-bottom: 24px; 51 | } 52 | 53 | h2, 54 | h3 { 55 | margin-bottom: 8px; 56 | color: $text-color; 57 | font-weight: 600; 58 | } 59 | 60 | .subtitle { 61 | color: $text-color-secondary; 62 | } 63 | } 64 | 65 | @keyframes fadeIn { 66 | from { 67 | opacity: 0; 68 | transform: translateY(10px); 69 | } 70 | to { 71 | opacity: 1; 72 | transform: translateY(0); 73 | } 74 | } 75 | 76 | .auth-card { 77 | user-select: none; 78 | animation: fadeIn 0.4s ease-in-out; 79 | } 80 | -------------------------------------------------------------------------------- /ui/src/styles/ConsentPage.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #1890ff; 2 | $background-color: #f5f5f5; 3 | $card-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 4 | $mobile-breakpoint: 768px; 5 | 6 | .consent-form-container { 7 | min-height: 100vh; 8 | background-color: $background-color; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | 13 | @media (min-width: $mobile-breakpoint) { 14 | padding: 40px 16px; 15 | } 16 | 17 | @media (max-width: $mobile-breakpoint) { 18 | padding: 0; 19 | } 20 | 21 | .title-container { 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | margin-bottom: 24px; 26 | margin-left: auto; 27 | margin-right: auto; 28 | text-align: center; 29 | } 30 | 31 | .card-title { 32 | @media (max-width: $mobile-breakpoint) { 33 | font-size: 1.5em !important; 34 | } 35 | 36 | @media (min-width: $mobile-breakpoint) { 37 | font-size: 1.8em !important; 38 | } 39 | 40 | user-select: none; 41 | } 42 | } 43 | 44 | .consent-card { 45 | width: 100%; 46 | display: flex; 47 | flex-direction: column; 48 | 49 | @media (min-width: $mobile-breakpoint) { 50 | border-radius: 8px; 51 | box-shadow: $card-shadow; 52 | max-width: 550px; 53 | padding: 32px; 54 | margin: auto; 55 | min-height: auto; 56 | justify-content: flex-start; 57 | } 58 | 59 | @media (max-width: $mobile-breakpoint) { 60 | border-radius: 0; 61 | box-shadow: none; 62 | max-width: 100%; 63 | height: 100vh; 64 | padding: 24px 16px; 65 | margin: 0; 66 | justify-content: center; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | --------------------------------------------------------------------------------