diff --git a/esignet-service/.env.example b/esignet-service/.env.example
index 7fa860af9..7827075d8 100644
--- a/esignet-service/.env.example
+++ b/esignet-service/.env.example
@@ -4,8 +4,10 @@
# Copy this file to .env and adjust values for your environment:
# cp .env.example .env
#
-# make.sh loads .env automatically when present.
-# Precedence: VAR=VALUE on the command line > .env > shell environment > defaults.
+# make.sh loads .env into the environment before starting the service, so these
+# values apply to `go run`, the binary, and tests. For Docker/CI, pass the same
+# variables through your usual mechanism. Variables already set in the real
+# environment always take precedence over .env.
# ─────────────────────────────────────────────────────────────────────────────
# ── HTTP / ThunderID engine ───────────────────────────────────────────────────
@@ -53,14 +55,14 @@ OIDC_UI_ERROR_PATH=/error
# ── PostgreSQL (client management persistence) ────────────────────────────────
# Option A: full DSN (takes precedence over individual vars)
-# POSTGRES_URL=postgres://esignet:secret@localhost:5432/mosip_esignet?sslmode=disable
+# DATABASE_URL=postgres://esignet:secret@localhost:5432/mosip_esignet?sslmode=disable
# Option B: individual connection params
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=mosip_esignet
DATABASE_USERNAME=esignet
-# DB_DBUSER_PASSWORD=secret
+# DATABASE_PASSWORD=secret
# Connection pool tuning (optional — defaults shown)
# DB_MAX_OPEN_CONNS=25
diff --git a/esignet-service/README.md b/esignet-service/README.md
index b9d5cba21..692df25ed 100644
--- a/esignet-service/README.md
+++ b/esignet-service/README.md
@@ -60,12 +60,14 @@ The checked-in `go.mod` `replace` directive pins a Thunder backend fork until th
### Quick start (development)
```bash
-cp .env.example .env # fill in DATABASE_* / DB_DBUSER_PASSWORD and REDIS_* at minimum
+cp .env.example .env # fill in DATABASE_* and REDIS_* at minimum
./make.sh run
```
Copy `.env.example` to `.env` to override defaults, or pass overrides on the command line (`./make.sh run PORT=9090`).
+`make.sh` loads `.env` from the working directory into the environment before starting the service, so it applies to `go run`, the built binary, and tests. The service itself only reads variables already present in the environment; for Docker/CI, supply them through your usual mechanism. Real environment variables already set always take precedence over `.env`.
+
### Binary
```bash
@@ -75,7 +77,7 @@ export PORT=8088
export MOSIP_ESIGNET_HOST=http://127.0.0.1:8088
export DATABASE_HOST=localhost
export DATABASE_USERNAME=esignet
-export DB_DBUSER_PASSWORD=secret
+export DATABASE_PASSWORD=secret
export DATABASE_NAME=mosip_esignet
export REDIS_HOST=localhost
export AUTHN_PROVIDER=mosip
@@ -139,12 +141,12 @@ Authorize redirects are sent to the Thunder gate client:
| Variable | Default | Purpose |
|----------|---------|---------|
-| `POSTGRES_URL` | _(empty)_ | Full DSN — takes precedence if set |
+| `DATABASE_URL` | _(empty)_ | Full DSN — takes precedence if set |
| `DATABASE_HOST` | `localhost` | |
| `DATABASE_PORT` | `5432` | |
| `DATABASE_NAME` | `mosip_esignet` | |
| `DATABASE_USERNAME` | `postgres` | |
-| `DB_DBUSER_PASSWORD` | _(empty)_ | |
+| `DATABASE_PASSWORD` | _(empty)_ | |
| `DB_MAX_OPEN_CONNS` | `25` | Max open connections |
| `DB_MAX_IDLE_CONNS` | `5` | Max idle connections |
| `DB_CONN_MAX_LIFETIME_SECS` | `300` | Connection lifetime |
@@ -339,7 +341,7 @@ curl -s http://127.0.0.1:8088/health
docker run --rm -p 8088:8088 \
-e MOSIP_ESIGNET_HOST=http://127.0.0.1:8088 \
-e CRYPTO_ENCRYPTION_KEY=your-64-char-hex-key \
- -e POSTGRES_URL=postgres://esignet:secret@host.docker.internal:5432/mosip_esignet?sslmode=disable \
+ -e DATABASE_URL=postgres://esignet:secret@host.docker.internal:5432/mosip_esignet?sslmode=disable \
-e REDIS_URL=redis://host.docker.internal:6379/0 \
-e AUTHN_PROVIDER=mosip \
esignet:latest
diff --git a/esignet-service/cmd/esignet/main.go b/esignet-service/cmd/esignet/main.go
index 0971e1da5..dad6a5e60 100644
--- a/esignet-service/cmd/esignet/main.go
+++ b/esignet-service/cmd/esignet/main.go
@@ -2,20 +2,28 @@
package main
import (
- "context"
+ "fmt"
"net/http"
"github.com/thunder-id/thunderid/pkg/thunderidengine"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
"github.com/mosip/esignet/internal/clientmgmt"
"github.com/mosip/esignet/internal/config"
"github.com/mosip/esignet/internal/engine"
+ "github.com/mosip/esignet/internal/engine/shared"
applog "github.com/mosip/esignet/internal/log"
)
func main() {
logger := applog.GetLogger()
- appCfg := config.LoadAppConfig()
+ appCfg, err := config.LoadAppConfig()
+ if err != nil {
+ logger.Fatal("failed to load app config", applog.Error(err))
+ }
+ if err := config.ApplyEnvOverrides(appCfg); err != nil {
+ logger.Fatal("failed to apply env overrides", applog.Error(err))
+ }
pgConn, err := appCfg.DB.Open()
if err != nil {
@@ -63,48 +71,47 @@ func main() {
})
clientHandler.RegisterRoutes(mux, scopeMW)
- hostCfg := engine.LoadConfig()
- thunderCfg, executors, err := engine.BuildThunderConfig(appCfg)
- if err != nil {
- logger.Fatal("build thunder config", applog.Error(err))
- }
- certFile, keyFile, err := engine.PKIPaths(appCfg.DataDir)
- if err != nil {
- logger.Fatal("signing key paths", applog.Error(err))
- }
- actorProvider := engine.NewActorProvider(clientSvc, hostCfg)
- roleProvider := engine.NewRoleProvider()
- logger.Info("authn provider selected", applog.String("provider", hostCfg.Provider))
- authnProvider, err := engine.NewAuthnProviderFromConfig(hostCfg.Provider, clientSvc)
+ authnProvider, err := engine.NewAuthnProvider(appCfg.Provider, appCfg, clientSvc)
if err != nil {
logger.Fatal("authn provider", applog.Error(err))
}
+ logger.Info("authn provider selected", applog.String("provider", appCfg.Provider))
- eng, err := thunderidengine.New(
- thunderidengine.WithRedis(redisClient, appCfg.Redis.KeyPrefix),
- thunderidengine.WithConfig(appCfg.DataDir, thunderCfg),
- thunderidengine.WithPKIKey("default-key", certFile, keyFile),
- thunderidengine.WithHostActorProvider(actorProvider),
- thunderidengine.WithHostAuthnProvider(authnProvider),
- thunderidengine.WithHostRoleProvider(roleProvider),
- thunderidengine.WithExecutorDependencies(thunderidengine.ExecutorDependencies{
- ConsentEnforcer: engine.NewConsentEnforcer(),
- }),
- thunderidengine.WithEnabledExecutors(executors...),
- thunderidengine.WithCustomExecutors(engine.CustomExecutors(authnProvider, hostCfg.Provider)),
+ _ = thunderidengine.New(mux,
+ thunderidengine.WithServerHome(appCfg.DataDir),
+ thunderidengine.WithServerConfig(appCfg.Server),
+ thunderidengine.WithCacheConfig(appCfg.Cache),
+ thunderidengine.WithOAuthConfig(appCfg.OAuth),
+ thunderidengine.WithJWTConfig(appCfg.JWT),
+ thunderidengine.WithFlowConfig(appCfg.Flow),
+ thunderidengine.WithObservabilityConfig(appCfg.Observability),
+ thunderidengine.WithActorProvider(engine.NewActorProvider(clientSvc, appCfg)),
+ thunderidengine.WithAuthnProvider(authnProvider),
+ thunderidengine.WithAuthorizationProvider(engine.NewAuthorizationProvider(appCfg)),
+ thunderidengine.WithConsentProvider(engine.NewConsentEnforcer()),
+ thunderidengine.WithDesignResolveProvider(engine.NewDesignProvider(appCfg)),
+ thunderidengine.WithFlowProvider(engine.NewFlowProvider(appCfg)),
+ thunderidengine.WithI18nProvider(engine.NewI18nProvider(appCfg)),
+ thunderidengine.WithOUProvider(engine.NewOUProvider(appCfg)),
+ thunderidengine.WithResourceProvider(engine.NewResourceProvider(appCfg)),
+ thunderidengine.WithObservabilityProvider(engine.NewObservabilityProvider(appCfg.Observability)),
+ thunderidengine.WithCustomExecutors(getCustomExecutors(authnProvider)),
)
- if err != nil {
- logger.Fatal("initialize engine", applog.Error(err))
- }
- defer func() { _ = eng.Shutdown(context.Background()) }()
-
- if err := eng.RegisterRoutes(mux); err != nil {
- logger.Fatal("register engine routes", applog.Error(err))
- }
- addr := ":" + appCfg.Port
+ addr := fmt.Sprintf(":%d", appCfg.Port)
logger.Info("server listening", applog.String("addr", addr), applog.String("issuer", appCfg.Issuer))
if err := http.ListenAndServe(addr, mux); err != nil {
logger.Fatal("server", applog.Error(err))
}
}
+
+// CustomExecutors returns embedder-supplied flow executors keyed by executor name.
+func getCustomExecutors(authn providers.AuthnProviderManager) map[string]providers.Executor {
+ otpAuthn, ok := authn.(shared.ConsolidatedAuthnProvider)
+ if !ok {
+ return map[string]providers.Executor{}
+ }
+ return map[string]providers.Executor{
+ engine.ExecutorNameMosipOTP: engine.NewMosipOtpExecutor(otpAuthn),
+ }
+}
diff --git a/esignet-service/data/config/resources/agents/agent-declarative-1.yaml b/esignet-service/data/config/resources/agents/agent-declarative-1.yaml
deleted file mode 100644
index d97e11470..000000000
--- a/esignet-service/data/config/resources/agents/agent-declarative-1.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-id: decl-agent-1
-ouId: decl-ou-1
-type: default
-name: Declarative Test Agent
-description: A declarative test agent for integration testing
-authFlowId: decl-flow-1
-attributes:
- department: engineering
- environment: test
diff --git a/esignet-service/data/config/resources/flows/flow-declarative-1.yaml b/esignet-service/data/config/resources/flows/flow-declarative-1.yaml
deleted file mode 100644
index 57c7eef09..000000000
--- a/esignet-service/data/config/resources/flows/flow-declarative-1.yaml
+++ /dev/null
@@ -1,50 +0,0 @@
-id: decl-flow-1
-handle: decl-flow-1
-name: Declarative Test Flow
-flowType: AUTHENTICATION
-activeVersion: 1
-nodes:
- - id: start
- type: START
- onSuccess: prompt_credentials
- - id: prompt_credentials
- type: PROMPT
- prompts:
- - inputs:
- - ref: input_001
- identifier: username
- type: TEXT_INPUT
- required: true
- - ref: input_002
- identifier: password
- type: PASSWORD_INPUT
- required: true
- action:
- ref: action_001
- nextNode: basic_auth
- - id: basic_auth
- type: TASK_EXECUTION
- executor:
- name: CredentialsAuthExecutor
- inputs:
- - ref: input_001
- identifier: username
- type: TEXT_INPUT
- required: true
- - ref: input_002
- identifier: password
- type: PASSWORD_INPUT
- required: true
- onSuccess: authorization_check
- - id: authorization_check
- type: TASK_EXECUTION
- executor:
- name: AuthorizationExecutor
- onSuccess: auth_assert
- - id: auth_assert
- type: TASK_EXECUTION
- executor:
- name: AuthAssertExecutor
- onSuccess: end
- - id: end
- type: END
diff --git a/esignet-service/data/config/resources/flows/flow-declarative-multi-flow-1.yaml b/esignet-service/data/config/resources/flows/flow-declarative-multi-flow-1.yaml
deleted file mode 100644
index 82fabfdd6..000000000
--- a/esignet-service/data/config/resources/flows/flow-declarative-multi-flow-1.yaml
+++ /dev/null
@@ -1,824 +0,0 @@
-id: flow-declarative-multi-flow-1
-name: Default Basic Authentication Flow
-handle: default-basic-multi-flow
-flowType: AUTHENTICATION
-nodes:
- - id: start
- type: START
- layout:
- size:
- width: 101
- height: 34
- position:
- x: 100
- y: 1050
- onSuccess: auth_factor
-
- - id: auth_factor
- type: PROMPT
- meta:
- components:
- - id: text_title
- type: TEXT
- label: Sign In
- variant: HEADING_3
- category: DISPLAY
- resourceType: ELEMENT
- - id: block_auth_options
- type: BLOCK
- category: BLOCK
- components:
- - id: action_otp
- type: ACTION
- label: Verify with OTP
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- - id: action_password
- type: ACTION
- label: Verify with Password
- variant: SECONDARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 280
- position:
- x: 350
- y: 1000
- prompts:
- - action:
- ref: action_otp
- nextNode: prompt_otp_phone
- - action:
- ref: action_password
- nextNode: prompt_pwd_phone
-
- - id: prompt_otp_phone
- type: PROMPT
- meta:
- components:
- - id: block_otp_phone
- type: BLOCK
- category: BLOCK
- components:
- - id: tab_phone
- type: ACTION
- label: Phone
- variant: PRIMARY
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_email
- type: ACTION
- label: Email
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_vid
- type: ACTION
- label: VID
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: input_phone
- ref: username
- hint: ""
- type: PHONE_INPUT
- label: Mobile Number
- category: FIELD
- required: true
- placeholder: Enter your mobile number
- postfix: "@phone"
- prefixes:
- - label: "+91"
- value: "+91"
- maxLength: 10
- regex: "^[0-9]{10}$"
- - label: "+855"
- value: "+855"
- maxLength: 9
- regex: "^[0-9]{9}$"
- validation:
- - type: regex
- value: "^[0-9]{10}$"
- message: "{{i18n(validation:phone.pattern.invalid)}}"
- - type: maxLength
- value: 10
- message: "{{i18n(validation:phone.maxLength.invalid)}}"
- resourceType: ELEMENT
- - id: action_continue
- type: ACTION
- label: Continue
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 460
- position:
- x: 850
- y: -100
- prompts:
- - action:
- ref: action_continue
- nextNode: task_sendotp
- inputs:
- - ref: input_phone
- type: PHONE_INPUT
- required: true
- identifier: username
- validation:
- - type: regex
- value: "^[0-9]{10}$"
- message: "{{i18n(validation:phone.pattern.invalid)}}"
- - type: maxLength
- value: 10
- message: "{{i18n(validation:phone.maxLength.invalid)}}"
- - action:
- ref: tab_email
- nextNode: prompt_otp_email
- inputs: []
- - action:
- ref: tab_vid
- nextNode: prompt_otp_vid
- inputs: []
-
- - id: prompt_otp_email
- type: PROMPT
- meta:
- components:
- - id: block_otp_email
- type: BLOCK
- category: BLOCK
- components:
- - id: tab_phone
- type: ACTION
- label: Phone
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_email
- type: ACTION
- label: Email
- variant: PRIMARY
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_vid
- type: ACTION
- label: VID
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: input_email
- ref: username
- hint: ""
- type: EMAIL_INPUT
- label: Email Address
- category: FIELD
- required: true
- placeholder: Enter your email address
- postfix: "@thunder.sky"
- validation:
- - type: regex
- value: "^[a-zA-Z0-9._%+\\-]+$"
- message: "{{i18n(validation:email.pattern.invalid)}}"
- - type: maxLength
- value: 64
- message: "{{i18n(validation:email.maxLength.invalid)}}"
- resourceType: ELEMENT
- - id: action_continue
- type: ACTION
- label: Continue
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 460
- position:
- x: 850
- y: 450
- prompts:
- - action:
- ref: tab_phone
- nextNode: prompt_otp_phone
- inputs: []
- - action:
- ref: action_continue
- nextNode: task_sendotp
- inputs:
- - ref: input_email
- type: EMAIL_INPUT
- required: true
- identifier: username
- validation:
- - type: regex
- value: "^[a-zA-Z0-9._%+\\-]+$"
- message: "{{i18n(validation:email.pattern.invalid)}}"
- - type: maxLength
- value: 64
- message: "{{i18n(validation:email.maxLength.invalid)}}"
- - action:
- ref: tab_vid
- nextNode: prompt_otp_vid
- inputs: []
-
- - id: prompt_otp_vid
- type: PROMPT
- meta:
- components:
- - id: block_otp_vid
- type: BLOCK
- category: BLOCK
- components:
- - id: tab_phone
- type: ACTION
- label: Phone
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_email
- type: ACTION
- label: Email
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_vid
- type: ACTION
- label: VID
- variant: PRIMARY
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: input_vid
- ref: username
- hint: ""
- type: TEXT_INPUT
- label: VID
- category: FIELD
- required: true
- inputType: text
- placeholder: Enter your VID
- validation:
- - type: regex
- value: "^[A-Z0-9]+$"
- message: "{{i18n(validation:vid.pattern.invalid)}}"
- - type: maxLength
- value: 20
- message: "{{i18n(validation:vid.maxLength.invalid)}}"
- resourceType: ELEMENT
- - id: action_continue
- type: ACTION
- label: Continue
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 460
- position:
- x: 850
- y: 1000
- prompts:
- - action:
- ref: tab_phone
- nextNode: prompt_otp_phone
- inputs: []
- - action:
- ref: tab_email
- nextNode: prompt_otp_email
- inputs: []
- - action:
- ref: action_continue
- nextNode: task_sendotp
- inputs:
- - ref: input_vid
- type: TEXT_INPUT
- required: true
- identifier: username
- validation:
- - type: regex
- value: "^[A-Z0-9]+$"
- message: "{{i18n(validation:vid.pattern.invalid)}}"
- - type: maxLength
- value: 20
- message: "{{i18n(validation:vid.maxLength.invalid)}}"
-
- - id: task_sendotp
- type: TASK_EXECUTION
- layout:
- size:
- width: 200
- height: 113
- position:
- x: 1900
- y: 500
- executor:
- name: MosipOtpExecutor
- inputs:
- - ref: username
- type: TEXT_INPUT
- required: true
- identifier: username
- onSuccess: prompt_enter_otp
-
- - id: prompt_enter_otp
- type: PROMPT
- meta:
- components:
- - id: text_otp_title
- type: TEXT
- label: Enter OTP
- variant: HEADING_3
- category: DISPLAY
- resourceType: ELEMENT
- - id: block_enter_otp
- type: BLOCK
- category: BLOCK
- components:
- - id: input_otp
- ref: otp
- hint: ""
- type: OTP_INPUT
- label: Please enter the 6-digit OTP sent to your registered contact
- category: FIELD
- required: true
- placeholder: ""
- validation:
- - type: regex
- value: "^[0-9]{6}$"
- message: "{{i18n(validation:otp.pattern.invalid)}}"
- resourceType: ELEMENT
- - id: action_verify
- type: ACTION
- label: Verify
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- - id: action_resend
- type: ACTION
- label: Resend OTP
- variant: TEXT
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 480
- position:
- x: 2350
- y: 450
- prompts:
- - action:
- ref: action_verify
- nextNode: task_verify_otp
- inputs:
- - ref: input_otp
- type: OTP_INPUT
- required: true
- identifier: otp
- validation:
- - type: regex
- value: "^[0-9]{6}$"
- message: "{{i18n(validation:otp.pattern.invalid)}}"
- - action:
- ref: action_resend
- nextNode: task_sendotp
- inputs: []
-
- - id: task_verify_otp
- type: TASK_EXECUTION
- layout:
- size:
- width: 217
- height: 113
- position:
- x: 2900
- y: 500
- executor:
- name: CredentialsAuthExecutor
- inputs:
- - ref: username
- type: TEXT_INPUT
- required: true
- identifier: username
- - ref: input_otp
- type: OTP_INPUT
- required: true
- identifier: otp
- onSuccess: authorization_check
- onFailure: prompt_enter_otp
- onIncomplete: prompt_otp_phone
-
- - id: prompt_pwd_phone
- type: PROMPT
- meta:
- components:
- - id: block_pwd_phone
- type: BLOCK
- category: BLOCK
- components:
- - id: tab_phone
- type: ACTION
- label: Phone
- variant: PRIMARY
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_email
- type: ACTION
- label: Email
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_vid
- type: ACTION
- label: VID
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: input_phone
- ref: username
- hint: ""
- type: PHONE_INPUT
- label: Mobile Number
- category: FIELD
- required: true
- placeholder: Enter your mobile number
- postfix: "@phone"
- prefixes:
- - label: "+91"
- value: "+91"
- maxLength: 10
- regex: "^[0-9]{10}$"
- - label: "+855"
- value: "+855"
- maxLength: 9
- regex: "^[0-9]{9}$"
- validation:
- - type: regex
- value: "^[0-9]{10}$"
- message: "{{i18n(validation:phone.pattern.invalid)}}"
- - type: maxLength
- value: 10
- message: "{{i18n(validation:phone.maxLength.invalid)}}"
- resourceType: ELEMENT
- - id: action_continue
- type: ACTION
- label: Continue
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 460
- position:
- x: 850
- y: 1600
- prompts:
- - action:
- ref: action_continue
- nextNode: prompt_enter_pwd
- inputs:
- - ref: input_phone
- type: PHONE_INPUT
- required: true
- identifier: username
- validation:
- - type: regex
- value: "^[0-9]{10}$"
- message: "{{i18n(validation:phone.pattern.invalid)}}"
- - type: maxLength
- value: 10
- message: "{{i18n(validation:phone.maxLength.invalid)}}"
- - action:
- ref: tab_email
- nextNode: prompt_pwd_email
- inputs: []
- - action:
- ref: tab_vid
- nextNode: prompt_pwd_vid
- inputs: []
-
- - id: prompt_pwd_email
- type: PROMPT
- meta:
- components:
- - id: block_pwd_email
- type: BLOCK
- category: BLOCK
- components:
- - id: tab_phone
- type: ACTION
- label: Phone
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_email
- type: ACTION
- label: Email
- variant: PRIMARY
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_vid
- type: ACTION
- label: VID
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: input_email
- ref: username
- hint: ""
- type: EMAIL_INPUT
- label: Email Address
- category: FIELD
- required: true
- placeholder: Enter your email address
- postfix: "@thunder.sky"
- validation:
- - type: regex
- value: "^[a-zA-Z0-9._%+\\-]+$"
- message: "{{i18n(validation:email.pattern.invalid)}}"
- - type: maxLength
- value: 64
- message: "{{i18n(validation:email.maxLength.invalid)}}"
- resourceType: ELEMENT
- - id: action_continue
- type: ACTION
- label: Continue
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 460
- position:
- x: 850
- y: 2150
- prompts:
- - action:
- ref: tab_phone
- nextNode: prompt_pwd_phone
- inputs: []
- - action:
- ref: action_continue
- nextNode: prompt_enter_pwd
- inputs:
- - ref: input_email
- type: EMAIL_INPUT
- required: true
- identifier: username
- validation:
- - type: regex
- value: "^[a-zA-Z0-9._%+\\-]+$"
- message: "{{i18n(validation:email.pattern.invalid)}}"
- - type: maxLength
- value: 64
- message: "{{i18n(validation:email.maxLength.invalid)}}"
- - action:
- ref: tab_vid
- nextNode: prompt_pwd_vid
- inputs: []
-
- - id: prompt_pwd_vid
- type: PROMPT
- meta:
- components:
- - id: block_pwd_vid
- type: BLOCK
- category: BLOCK
- components:
- - id: tab_phone
- type: ACTION
- label: Phone
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_email
- type: ACTION
- label: Email
- variant: OUTLINED
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: tab_vid
- type: ACTION
- label: VID
- variant: PRIMARY
- category: ACTION
- eventType: TRIGGER
- resourceType: ELEMENT
- - id: input_vid
- ref: username
- hint: ""
- type: TEXT_INPUT
- label: VID
- category: FIELD
- required: true
- inputType: text
- placeholder: Enter your VID
- validation:
- - type: regex
- value: "^[A-Z0-9]+$"
- message: "{{i18n(validation:vid.pattern.invalid)}}"
- - type: maxLength
- value: 20
- message: "{{i18n(validation:vid.maxLength.invalid)}}"
- resourceType: ELEMENT
- - id: action_continue
- type: ACTION
- label: Continue
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 460
- position:
- x: 850
- y: 2700
- prompts:
- - action:
- ref: tab_phone
- nextNode: prompt_pwd_phone
- inputs: []
- - action:
- ref: tab_email
- nextNode: prompt_pwd_email
- inputs: []
- - action:
- ref: action_continue
- nextNode: prompt_enter_pwd
- inputs:
- - ref: input_vid
- type: TEXT_INPUT
- required: true
- identifier: username
- validation:
- - type: regex
- value: "^[A-Z0-9]+$"
- message: "{{i18n(validation:vid.pattern.invalid)}}"
- - type: maxLength
- value: 20
- message: "{{i18n(validation:vid.maxLength.invalid)}}"
-
- - id: prompt_enter_pwd
- type: PROMPT
- meta:
- components:
- - id: text_pwd_title
- type: TEXT
- label: Enter Password
- variant: HEADING_3
- category: DISPLAY
- resourceType: ELEMENT
- - id: block_enter_pwd
- type: BLOCK
- category: BLOCK
- components:
- - id: input_password
- ref: password
- hint: ""
- type: PASSWORD_INPUT
- label: Password
- category: FIELD
- required: true
- placeholder: Enter your password
- validation:
- - type: minLength
- value: 8
- message: "{{i18n(validation:password.minLength.invalid)}}"
- - type: maxLength
- value: 128
- message: "{{i18n(validation:password.maxLength.invalid)}}"
- resourceType: ELEMENT
- - id: action_login
- type: ACTION
- label: Login
- variant: PRIMARY
- category: ACTION
- eventType: SUBMIT
- resourceType: ELEMENT
- resourceType: ELEMENT
- layout:
- size:
- width: 350
- height: 360
- position:
- x: 1900
- y: 2200
- prompts:
- - action:
- ref: action_login
- nextNode: task_verify_pwd
- inputs:
- - ref: input_password
- type: PASSWORD_INPUT
- required: true
- identifier: password
- validation:
- - type: minLength
- value: 8
- message: "{{i18n(validation:password.minLength.invalid)}}"
- - type: maxLength
- value: 128
- message: "{{i18n(validation:password.maxLength.invalid)}}"
-
- - id: task_verify_pwd
- type: TASK_EXECUTION
- layout:
- size:
- width: 217
- height: 113
- position:
- x: 2450
- y: 2250
- executor:
- name: CredentialsAuthExecutor
- inputs:
- - ref: username
- type: TEXT_INPUT
- required: true
- identifier: username
- - ref: input_password
- type: PASSWORD_INPUT
- required: true
- identifier: password
- onSuccess: authorization_check
- onFailure: prompt_enter_pwd
- onIncomplete: prompt_pwd_phone
-
- - id: authorization_check
- type: TASK_EXECUTION
- layout:
- size:
- width: 200
- height: 113
- position:
- x: 3200
- y: 1150
- executor:
- name: AuthorizationExecutor
- onSuccess: auth_assert
-
- - id: auth_assert
- type: TASK_EXECUTION
- layout:
- size:
- width: 244
- height: 113
- position:
- x: 3500
- y: 1150
- executor:
- name: AuthAssertExecutor
- onSuccess: end
-
- - id: end
- type: END
- layout:
- size:
- width: 85
- height: 34
- position:
- x: 3850
- y: 1167
diff --git a/esignet-service/data/config/resources/flows/flow-declarative-recovery-1.yaml b/esignet-service/data/config/resources/flows/flow-declarative-recovery-1.yaml
deleted file mode 100644
index 4a0543aae..000000000
--- a/esignet-service/data/config/resources/flows/flow-declarative-recovery-1.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-id: decl-recovery-flow-1
-handle: decl-recovery-flow-1
-name: Declarative Test Recovery Flow
-flowType: RECOVERY
-activeVersion: 1
-nodes:
- - id: start
- type: START
- onSuccess: prompt_username
- - id: prompt_username
- type: PROMPT
- prompts:
- - inputs:
- - ref: input_username
- identifier: username
- type: TEXT_INPUT
- required: true
- action:
- ref: action_submit_username
- nextNode: identify_user
- - id: identify_user
- type: TASK_EXECUTION
- executor:
- name: IdentifyingExecutor
- mode: identify
- onSuccess: generate_recovery_token
- onFailure: end
- - id: generate_recovery_token
- type: TASK_EXECUTION
- executor:
- name: InviteExecutor
- mode: generate
- onSuccess: end
- - id: end
- type: END
diff --git a/esignet-service/data/config/resources/flows/flow-declarative-registration-1.yaml b/esignet-service/data/config/resources/flows/flow-declarative-registration-1.yaml
deleted file mode 100644
index 0d5607c81..000000000
--- a/esignet-service/data/config/resources/flows/flow-declarative-registration-1.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-id: decl-reg-flow-1
-handle: decl-reg-flow-1
-name: Declarative Test Registration Flow
-flowType: REGISTRATION
-activeVersion: 1
-nodes:
- - id: start
- type: START
- - id: login
- type: BASIC_AUTHENTICATION
- - id: end
- type: END
diff --git a/esignet-service/data/config/resources/flows/flow-declarative-sunbird-1.yaml b/esignet-service/data/config/resources/flows/flow-declarative-sunbird-1.yaml
deleted file mode 100644
index f25255e95..000000000
--- a/esignet-service/data/config/resources/flows/flow-declarative-sunbird-1.yaml
+++ /dev/null
@@ -1,58 +0,0 @@
-id: decl-sunbird-flow-1
-handle: decl-sunbird-flow-1
-name: SunbirdRC KBI Flow
-flowType: AUTHENTICATION
-activeVersion: 1
-nodes:
- - id: start
- type: START
- onSuccess: prompt_credentials
- - id: prompt_credentials
- type: PROMPT
- prompts:
- - inputs:
- - ref: input_001
- identifier: individualId
- type: TEXT_INPUT
- required: true
- - ref: input_002
- identifier: fullName
- type: TEXT_INPUT
- required: true
- - ref: input_003
- identifier: dob
- type: TEXT_INPUT
- required: true
- action:
- ref: action_001
- nextNode: basic_auth
- - id: basic_auth
- type: TASK_EXECUTION
- executor:
- name: CredentialsAuthExecutor
- inputs:
- - ref: input_001
- identifier: individualId
- type: TEXT_INPUT
- required: true
- - ref: input_002
- identifier: fullName
- type: PASSWORD_INPUT
- required: true
- - ref: input_003
- identifier: dob
- type: PASSWORD_INPUT
- required: true
- onSuccess: authorization_check
- - id: authorization_check
- type: TASK_EXECUTION
- executor:
- name: AuthorizationExecutor
- onSuccess: auth_assert
- - id: auth_assert
- type: TASK_EXECUTION
- executor:
- name: AuthAssertExecutor
- onSuccess: end
- - id: end
- type: END
diff --git a/esignet-service/data/config/resources/identity_providers/idp-declarative-1.yaml b/esignet-service/data/config/resources/identity_providers/idp-declarative-1.yaml
deleted file mode 100644
index 21456b7c5..000000000
--- a/esignet-service/data/config/resources/identity_providers/idp-declarative-1.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-id: decl-idp-1
-name: Declarative Test IDP
-description: A declarative test identity provider
-type: GOOGLE
-properties:
- - name: client_id
- value: test-client-id
- is_secret: false
- - name: client_secret
- value: test-client-secret
- is_secret: true
- - name: redirect_uri
- value: https://localhost:8095/callback
- is_secret: false
diff --git a/esignet-service/data/config/resources/organization_units/ou-declarative-1.yaml b/esignet-service/data/config/resources/organization_units/ou-declarative-1.yaml
deleted file mode 100644
index 4ea17f376..000000000
--- a/esignet-service/data/config/resources/organization_units/ou-declarative-1.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-id: decl-ou-1
-handle: decl-ou-1
-name: Declarative Test OU
-description: A declarative test organization unit for integration testing
diff --git a/esignet-service/data/config/resources/resource_servers/resource-declarative-1.yaml b/esignet-service/data/config/resources/resource_servers/resource-declarative-1.yaml
deleted file mode 100644
index 44b48bd29..000000000
--- a/esignet-service/data/config/resources/resource_servers/resource-declarative-1.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-id: decl-rs-1
-name: Declarative Resource Server
-description: A declarative test resource server
-handle: decl-rs-1
-ouId: decl-ou-1
-delimiter: ":"
-resources:
- - name: Test Resource
- handle: test-resource
- description: A test resource
- actions:
- - name: Read
- handle: read
- - name: Write
- handle: write
diff --git a/esignet-service/data/config/resources/roles/role-declarative-1.yaml b/esignet-service/data/config/resources/roles/role-declarative-1.yaml
deleted file mode 100644
index e2fa4fae8..000000000
--- a/esignet-service/data/config/resources/roles/role-declarative-1.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-id: decl-role-1
-name: Declarative Test Role
-description: A declarative test role for integration testing
-ouId: decl-ou-1
-permissions: []
-assignments: []
diff --git a/esignet-service/data/config/resources/user_types/usertype-declarative-1.yaml b/esignet-service/data/config/resources/user_types/usertype-declarative-1.yaml
deleted file mode 100644
index edb284099..000000000
--- a/esignet-service/data/config/resources/user_types/usertype-declarative-1.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-id: decl-schema-1
-name: Declarative Test Schema
-ouId: decl-ou-1
-allowSelfRegistration: false
-systemAttributes:
- display: "username"
-schema: |
- {
- "email": {
- "type": "string"
- },
- "username": {
- "type": "string"
- }
- }
diff --git a/esignet-service/data/config/resources/users/user-declarative-1.yaml b/esignet-service/data/config/resources/users/user-declarative-1.yaml
deleted file mode 100644
index 757c4752a..000000000
--- a/esignet-service/data/config/resources/users/user-declarative-1.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-id: decl-user-1
-type: person
-ouId: decl-ou-1
-attributes:
- username: decl-user-1
- email: decl-user-1@example.com
- given_name: Declarative
- family_name: User
-credentials:
- password: "TempPassword123!"
diff --git a/esignet-service/data/deployment.yaml b/esignet-service/data/deployment.yaml
index ffe3b26a0..1e50c5768 100644
--- a/esignet-service/data/deployment.yaml
+++ b/esignet-service/data/deployment.yaml
@@ -1,21 +1,66 @@
-declarative_resources:
- enabled: true
-flow:
- store: declarative
-resource:
- store: declarative
-organization_unit:
- store: declarative
-identity_provider:
- store: declarative
-role:
- store: declarative
-theme:
- store: declarative
-layout:
- store: declarative
-translation:
- store: declarative
+identifier: "${NAMESPACE}"
+port: 8088
+issuer: "${MOSIP_ESIGNET_HOST:-http://127.0.0.1:8088}"
+data_dir: "${DATA_DIR:-./data}"
+
+provider: "${AUTHN_PROVIDER:-mock}"
+auth_flow_id: "${DEFAULT_AUTH_FLOW_ID:-flow-esignet}"
+theme_id: "${DEFAULT_THEME_ID:-theme-esignet}"
+layout_id: "${DEFAULT_LAYOUT_ID:-layout-esignet}"
+
+server:
+ hostname: "0.0.0.0"
+ port: 8088
+ http_only: true
+ public_url: "${MOSIP_ESIGNET_HOST:-http://127.0.0.1:8088}"
+ identifier: ${NAMESPACE:-esignet}
+
+redis:
+ host: "${REDIS_HOST:-localhost}"
+ port: "${REDIS_PORT:-6379}"
+ password: "${REDIS_PASSWORD:-}"
+ db: ${REDIS_DB:-0}
+ tls: ${REDIS_TLS:-false}
+
+db:
+ dsn: "${DATABASE_URL}"
+ pool:
+ max_open_conns: ${DATABASE_MAX_OPEN_CONNS:-10}
+ max_idle_conns: ${DATABASE_MAX_IDLE_CONNS:-10}
+ conn_max_lifetime: ${DATABASE_CONN_MAX_LIFETIME:-0}
+ conn_max_idle_time: ${DATABASE_CONN_MAX_IDLE_TIME:-300}
+
+cache:
+ disabled: false
+ type: redis
+ size: 1000
+ ttl: 3600
+ eviction_policy: LRU
+ cleanup_interval: 300
+ redis:
+ address: "${REDIS_HOST}:${REDIS_PORT}"
+ db: ${REDIS_DB:-0}
+ key_prefix: "${NAMESPACE}:"
+
+gate_client:
+ scheme: http
+ hostname: "127.0.0.1"
+ port: 3000
+ login_path: /signin
+ error_path: /error
+
+jwt:
+ preferred_key_id: default-key
+ audience: application
+ validity_period: 3600
+
+oauth:
+ refresh_token:
+ renew_on_grant: false
+ authorization_code:
+ validity_period: 120
+ par:
+ expires_in: 3600
+
crypto:
- encryption:
- key: "${CRYPTO_ENCRYPTION_KEY}"
+ key: "${CRYPTO_ENCRYPTION_KEY}"
diff --git a/esignet-service/data/flows/flow-esignet.yaml b/esignet-service/data/flows/flow-esignet.yaml
new file mode 100644
index 000000000..c7c19df87
--- /dev/null
+++ b/esignet-service/data/flows/flow-esignet.yaml
@@ -0,0 +1,441 @@
+# Anchor for heading details
+# it contains heading & subheading details
+# logo of main application & client
+# one divider line as well
+heading_details: &heading_details
+ id: heading_details
+ resourceType: ELEMENT
+ type: BLOCK
+ category: BLOCK
+ align: center
+ components:
+ - category: DISPLAY
+ id: text_heading
+ label: Login using eSignet
+ resourceType: ELEMENT
+ type: TEXT
+ variant: HEADING_3
+ align: center
+ - category: DISPLAY
+ id: text_subheading
+ label: "
{{meta(application.name)}} is requesting authentication for login"
+ resourceType: ELEMENT
+ type: RICH_TEXT
+ variant: SUBTITLE_2
+ align: center
+ - id: logo_block
+ type: STACK
+ resourceType: ELEMENT
+ direction: col
+ justify: center
+ components:
+ - category: DISPLAY
+ id: client_logo
+ resourceType: ELEMENT
+ type: IMAGE
+ src: "{{meta(application.logoUrl)}}"
+ alt: "{{t(signin:images.app_logo.alt)}}"
+ height: 52px
+ width: 52px
+ - category: DISPLAY
+ id: logo_divider
+ label: "⇋"
+ resourceType: ELEMENT
+ type: RICH_TEXT
+ height: 24px
+ width: 24px
+ - category: DISPLAY
+ id: app_logo
+ resourceType: ELEMENT
+ type: IMAGE
+ src: "/logo.png"
+ alt: "{{t(signin:images.app_logo.alt)}}"
+ height: 52px
+ width: 52px
+ - id: divider_block
+ type: DIVIDER
+
+# Anchor for UIN input, with validation
+prompt_uin: &prompt_uin
+ id: username_input
+ ref: username
+ type: TEXT_INPUT
+ label: UIN/VID
+ required: true
+ placeholder: Enter your UIN/VID
+ validation:
+ - type: maxLength
+ value: 16
+ message: UIN/VID must be between 10 and 16 digits
+ - type: minLength
+ value: 10
+ message: UIN/VID must be between 10 and 16 digits
+ - type: regex
+ value: "[0-9]*"
+ message: UIN/VID must contain numbers only
+
+# Anchor for Submit button
+# default id is submit_uin, it can be changed
+# default label is COntinue, that also can be changed
+submit_button: &submit_button
+ id: submit_uin
+ label: Continue
+ type: ACTION
+ variant: PRIMARY
+ category: ACTION
+ eventType: SUBMIT
+ resourceType: ELEMENT
+
+# Anchor for Trigger button
+trigger_button: &trigger_button
+ <<: *submit_button
+ variant: OUTLINE
+ eventType: TRIGGER
+
+# Anchor for title for the page
+page_title: &page_title
+ id: acr_text_heading
+ type: TEXT
+ label: Select a Login ID Option
+ align: left
+ variant: HEADING_5
+ category: DISPLAY
+ resourceType: ELEMENT
+
+id: flow-esignet
+handle: flow-esignet
+name: MOSIP Esignet Flow
+flowType: AUTHENTICATION
+nodes:
+ - id: start
+ type: START
+ onSuccess: prompt_acr
+
+ - id: prompt_acr
+ type: PROMPT
+ meta:
+ components:
+ - *heading_details
+ - id: acr_block
+ type: BLOCK
+ resourceType: ELEMENT
+ category: BLOCK
+ direction: col
+ justify: center
+ components:
+ - <<: *page_title
+ label: Select a preferred mode of login
+ - <<: *trigger_button
+ id: acr_otp
+ label: Login with OTP
+ startIcon: "/images/otp_icon.svg"
+ - <<: *trigger_button
+ id: acr_password
+ label: Login with Password
+ startIcon: "/images/pwd_icon.svg"
+ - <<: *trigger_button
+ id: acr_bio
+ label: Login with Biometrics
+ startIcon: "/images/bio_icon.svg"
+ - <<: *trigger_button
+ id: acr_kbi
+ label: Login with KBI
+ startIcon: "/images/kbi_icon.svg"
+ prompts:
+ - action:
+ ref: acr_otp
+ nextNode: prompt_uin_otp
+ - action:
+ ref: acr_password
+ nextNode: prompt_credentials
+ - action:
+ ref: acr_bio
+ nextNode: prompt_uin_bio
+ - action:
+ ref: acr_kbi
+ nextNode: prompt_kbi_details
+
+ - id: prompt_uin_otp
+ type: PROMPT
+ meta:
+ components:
+ - *heading_details
+ - type: BLOCK
+ id: block_otp
+ components:
+ - <<: *page_title
+ id: title_otp
+ - *prompt_uin
+ - <<: *submit_button
+ id: submit_uin
+ label: Get OTP
+ prompts:
+ - inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ action:
+ ref: submit_uin
+ nextNode: send_mosip_otp
+
+ - id: send_mosip_otp
+ type: TASK_EXECUTION
+ executor:
+ name: MosipOtpExecutor
+ onSuccess: prompt_otp
+
+ - id: prompt_otp
+ type: PROMPT
+ meta:
+ components:
+ - *heading_details
+ - id: block_otp
+ type: BLOCK
+ category: BLOCK
+ components:
+ - <<: *page_title
+ id: title_otp
+ label: Enter OTP
+ - id: otp_input
+ ref: otp
+ type: OTP_INPUT
+ label: Please enter the 6-digit OTP sent to your registered contact
+ category: FIELD
+ required: true
+ - <<: *submit_button
+ id: action_submit_otp
+ label: Verify
+ - id: resend_otp_block
+ type: BLOCK
+ category: BLOCK
+ resourceType: ELEMENT
+ components:
+ - id: RESEND_OTP
+ type: CUSTOM
+ ref: resend_otp_timer
+ category: MISCELLANEOUS
+ resourceType: ELEMENT
+ timeLeft: 180
+ variant: OUTLINE
+ label: Resend OTP
+ prompts:
+ - inputs:
+ - ref: otp_input
+ identifier: otp
+ type: TEXT_INPUT
+ required: true
+ action:
+ ref: action_submit_otp
+ nextNode: basic_auth
+ - action:
+ ref: RESEND_OTP
+ nextNode: send_mosip_otp
+
+ - id: basic_auth
+ type: TASK_EXECUTION
+ executor:
+ name: CredentialsAuthExecutor
+ inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ - ref: otp_input
+ identifier: otp
+ type: PASSWORD_INPUT
+ required: true
+ onSuccess: authorization_check
+
+ - id: prompt_credentials
+ type: PROMPT
+ meta:
+ components:
+ - *heading_details
+ - id: block_credentials
+ resourceType: ELEMENT
+ type: BLOCK
+ category: BLOCK
+ components:
+ - <<: *page_title
+ id: title_credentials
+ - *prompt_uin
+ - id: password_input
+ ref: password
+ type: PASSWORD_INPUT
+ category: FIELD
+ inputType: password
+ label: Password
+ placeholder: Enter your password
+ required: true
+ resourceType: ELEMENT
+ - <<: *submit_button
+ id: password_authenticate
+ label: Login
+ prompts:
+ - inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ - ref: password_input
+ identifier: password
+ type: PASSWORD_INPUT
+ required: true
+ action:
+ ref: password_authenticate
+ nextNode: basic_cred_auth
+
+ - id: basic_cred_auth
+ type: TASK_EXECUTION
+ executor:
+ name: CredentialsAuthExecutor
+ inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ - ref: password_input
+ identifier: password
+ type: PASSWORD_INPUT
+ required: true
+ onSuccess: authorization_check
+
+ - id: prompt_uin_bio
+ type: PROMPT
+ meta:
+ components:
+ - *heading_details
+ - type: BLOCK
+ id: block_biometrics
+ components:
+ - <<: *page_title
+ id: title_biometrics
+ - *prompt_uin
+ - *submit_button
+ prompts:
+ - inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ action:
+ ref: submit_uin
+ nextNode: prompt_biometric
+
+ - id: prompt_biometric
+ type: PROMPT
+ meta:
+ components:
+ - *heading_details
+ - id: block_biometrics
+ type: BLOCK
+ components:
+ - <<: *page_title
+ id: title_biometrics
+ - id: SBI_ID
+ ref: biometrics
+ type: CUSTOM
+ category: MISCELLANEOUS
+ resourceType: ELEMENT
+ prompts:
+ - inputs:
+ - ref: SBI_ID
+ identifier: biometrics
+ type: CUSTOM
+ required: true
+ action:
+ ref: SBI_ID
+ nextNode: basic_bio_auth
+
+ - id: basic_bio_auth
+ type: TASK_EXECUTION
+ executor:
+ name: CredentialsAuthExecutor
+ inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ - ref: SBI_ID
+ identifier: biometrics
+ type: CUSTOM
+ required: true
+ onSuccess: authorization_check
+
+ - id: prompt_kbi_details
+ type: PROMPT
+ meta:
+ components:
+ - *heading_details
+ - id: block_kbi
+ type: BLOCK
+ components:
+ - <<: *page_title
+ id: title_kbi
+ - *prompt_uin
+ - id: fullname_input
+ ref: fullname
+ type: TEXT_INPUT
+ label: Fullname
+ required: true
+ placeholder: Enter your full name
+ - id: dob_input
+ ref: date_of_birth
+ type: DATE_INPUT
+ label: Date of Birth
+ required: true
+ placeholder: Select your date of birth
+ dateFormat: "yyyy-MM-dd"
+ - <<: *submit_button
+ id: submit_kbi_details
+ label: Login
+ prompts:
+ - inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ - ref: fullname_input
+ identifier: fullname
+ type: TEXT_INPUT
+ required: true
+ - ref: dob_input
+ identifier: date_of_birth
+ type: DATE_INPUT
+ required: true
+ action:
+ ref: submit_kbi_details
+ nextNode: basic_kbi_auth
+
+ - id: basic_kbi_auth
+ type: TASK_EXECUTION
+ executor:
+ name: CredentialsAuthExecutor
+ inputs:
+ - ref: username_input
+ identifier: username
+ type: TEXT_INPUT
+ required: true
+ - ref: fullname_input
+ identifier: fullname
+ type: TEXT_INPUT
+ required: true
+ - ref: dob_input
+ identifier: date_of_birth
+ type: TEXT_INPUT
+ required: true
+ onSuccess: authorization_check
+
+ - id: authorization_check
+ type: TASK_EXECUTION
+ executor:
+ name: AuthorizationExecutor
+ onSuccess: auth_assert
+ - id: auth_assert
+ type: TASK_EXECUTION
+ executor:
+ name: AuthAssertExecutor
+ onSuccess: end
+ - id: end
+ type: END
\ No newline at end of file
diff --git a/esignet-service/data/config/resources/flows/flow-declarative-mosip-otp-1.yaml b/esignet-service/data/flows/otp-flow.yaml
similarity index 98%
rename from esignet-service/data/config/resources/flows/flow-declarative-mosip-otp-1.yaml
rename to esignet-service/data/flows/otp-flow.yaml
index adcaf32ab..d769cea89 100644
--- a/esignet-service/data/config/resources/flows/flow-declarative-mosip-otp-1.yaml
+++ b/esignet-service/data/flows/otp-flow.yaml
@@ -1,6 +1,6 @@
-id: decl-mosip-otp-flow-1
-handle: decl-mosip-otp-flow-1
-name: MOSIP Send OTP Flow
+id: otp-flow
+handle: otp-flow
+name: eSignet OTP Flow
flowType: AUTHENTICATION
activeVersion: 1
nodes:
@@ -211,4 +211,4 @@ nodes:
onSuccess: end
- id: end
- type: END
+ type: END
\ No newline at end of file
diff --git a/esignet-service/data/config/resources/layouts/layout-declarative-1.yaml b/esignet-service/data/layouts/layout-esignet.yaml
similarity index 53%
rename from esignet-service/data/config/resources/layouts/layout-declarative-1.yaml
rename to esignet-service/data/layouts/layout-esignet.yaml
index fefe6a41e..d2ab1128c 100644
--- a/esignet-service/data/config/resources/layouts/layout-declarative-1.yaml
+++ b/esignet-service/data/layouts/layout-esignet.yaml
@@ -1,6 +1,6 @@
-id: decl-layout-1
-displayName: Declarative Test Layout
-description: A declarative test layout
+id: layout-esignet
+displayName: Esignet Layout
+description: A Esignet layout
layout:
type: centered
components:
@@ -9,4 +9,4 @@ layout:
- type: form
position: center
- type: footer
- position: bottom
+ position: bottom
\ No newline at end of file
diff --git a/esignet-service/data/config/resources/themes/theme-declarative-1.yaml b/esignet-service/data/themes/theme-esignet.yaml
similarity index 99%
rename from esignet-service/data/config/resources/themes/theme-declarative-1.yaml
rename to esignet-service/data/themes/theme-esignet.yaml
index 1de8ba6d1..478b942a8 100644
--- a/esignet-service/data/config/resources/themes/theme-declarative-1.yaml
+++ b/esignet-service/data/themes/theme-esignet.yaml
@@ -505,4 +505,4 @@ theme:
- linear-gradient(rgba(255 255 255 / 0.159), rgba(255 255 255 / 0.159))
- linear-gradient(rgba(255 255 255 / 0.161), rgba(255 255 255 / 0.161))
- linear-gradient(rgba(255 255 255 / 0.163), rgba(255 255 255 / 0.163))
- - linear-gradient(rgba(255 255 255 / 0.165), rgba(255 255 255 / 0.165))
+ - linear-gradient(rgba(255 255 255 / 0.165), rgba(255 255 255 / 0.165))
\ No newline at end of file
diff --git a/esignet-service/go.mod b/esignet-service/go.mod
index 8589a5ac9..b1f66d8df 100644
--- a/esignet-service/go.mod
+++ b/esignet-service/go.mod
@@ -4,15 +4,16 @@ go 1.26
require (
github.com/golang-jwt/jwt/v5 v5.3.0
+ github.com/kelseyhightower/envconfig v1.4.0
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.18.0
github.com/stretchr/testify v1.11.1
- github.com/thunder-id/thunderid v0.0.0
- software.sslmate.com/src/go-pkcs12 v0.7.1
+ github.com/thunder-id/thunderid v0.0.0-20260701124159-a8f0fb52b5c5
+ golang.org/x/text v0.32.0
+ gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -26,7 +27,6 @@ require (
github.com/google/go-tpm v0.9.6 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/google/uuid v1.6.0 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modelcontextprotocol/go-sdk v1.4.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
@@ -38,29 +38,18 @@ require (
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
- go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
- go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
- go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.42.0 // indirect
- golang.org/x/text v0.32.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
- google.golang.org/grpc v1.79.3 // indirect
- google.golang.org/protobuf v1.36.10 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.65.2 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.10.0 // indirect
modernc.org/sqlite v1.37.0 // indirect
)
-replace github.com/thunder-id/thunderid => github.com/anushasunkada/thunder/backend v0.0.0-20260622173216-af8eca3b191b
+replace github.com/thunder-id/thunderid => github.com/thunder-id/thunderid/backend v0.0.0-20260701124159-a8f0fb52b5c5
diff --git a/esignet-service/go.sum b/esignet-service/go.sum
index a248866d8..270890835 100644
--- a/esignet-service/go.sum
+++ b/esignet-service/go.sum
@@ -1,7 +1,5 @@
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
-github.com/anushasunkada/thunder/backend v0.0.0-20260622173216-af8eca3b191b h1:kSvE3aBpZiP1N8dg7fnBpY0Vsb9UUGDQwPaZ3VifFsI=
-github.com/anushasunkada/thunder/backend v0.0.0-20260622173216-af8eca3b191b/go.mod h1:bHFQmUiriPo26b8AYbAqc0yVjqdTYNuiDTpz10Q9OiU=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -31,8 +29,6 @@ github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
@@ -45,6 +41,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
+github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -75,6 +73,8 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/thunder-id/thunderid/backend v0.0.0-20260701124159-a8f0fb52b5c5 h1:GwBDw8G2ON3oQmUExAcbJJ1zaUuzsbXC5RyCIw5X2+s=
+github.com/thunder-id/thunderid/backend v0.0.0-20260701124159-a8f0fb52b5c5/go.mod h1:bHFQmUiriPo26b8AYbAqc0yVjqdTYNuiDTpz10Q9OiU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
@@ -95,16 +95,12 @@ go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWv
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
-go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
-go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
-go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
-go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
@@ -126,8 +122,6 @@ golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
-gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
-gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
@@ -165,5 +159,3 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-software.sslmate.com/src/go-pkcs12 v0.7.1 h1:bxkUPRsvTPNRBZa4M/aSX4PyMOEbq3V8I6hbkG4F4Q8=
-software.sslmate.com/src/go-pkcs12 v0.7.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
diff --git a/esignet-service/internal/config/app.go b/esignet-service/internal/config/app.go
index d91ecf217..0cbf476a4 100644
--- a/esignet-service/internal/config/app.go
+++ b/esignet-service/internal/config/app.go
@@ -2,62 +2,233 @@
package config
import (
+ "errors"
"fmt"
"os"
- "strconv"
+ "path/filepath"
"strings"
+
+ "github.com/kelseyhightower/envconfig"
+ engineconfig "github.com/thunder-id/thunderid/pkg/thunderidengine/config"
+ "gopkg.in/yaml.v3"
)
const (
- defaultPort = "8088"
- defaultDataDir = "./data"
+ defaultDataDir = "./data"
+ appConfigFileName = "deployment.yaml"
+ defaultGatePort = 3000
+ defaultSigningKeyPath = "./keys/signing.key"
+ defaultSigningCertPath = "./keys/signing.crt"
+ defaultGateScheme = "http"
+ defaultGateHostname = "127.0.0.1"
+ defaultGateLoginPath = "/signin"
+ defaultGateErrorPath = "/error"
)
// AppConfig holds core HTTP and infrastructure settings for the service.
type AppConfig struct {
- Port string
- Issuer string
- DataDir string
- DB DB
- Redis Redis
+ Identifier string `yaml:"identifier"`
+ Port int `yaml:"port"`
+ Issuer string `yaml:"issuer"`
+ DataDir string `yaml:"data_dir"`
+ DB DB `yaml:"db"`
+ Redis Redis `yaml:"redis"`
+ Provider string `yaml:"provider"`
+ AuthFlowID string `yaml:"auth_flow_id"`
+ ThemeID string `yaml:"theme_id"`
+ LayoutID string `yaml:"layout_id"`
+ Server engineconfig.ServerConfig `yaml:"server"`
+ Cache engineconfig.CacheConfig `yaml:"cache"`
+ OAuth engineconfig.OAuthConfig `yaml:"oauth"`
+ JWT engineconfig.JWTConfig `yaml:"jwt"`
+ Flow engineconfig.FlowConfig `yaml:"flow"`
+ Observability engineconfig.ObservabilityConfig `yaml:"observability"`
+ GateClient engineconfig.GateClientConfig `yaml:"gate_client"`
+ EncryptionConfig engineconfig.EncryptionConfig `yaml:"crypto"`
+ KeyConfig engineconfig.KeyConfig `yaml:"key"`
+ Consent engineconfig.ConsentConfig `yaml:"consent"`
}
-// LoadAppConfig reads application settings from the environment.
-func LoadAppConfig() AppConfig {
- port := envOrDefault("PORT", defaultPort)
- return AppConfig{
- Port: port,
- Issuer: envOrDefault("MOSIP_ESIGNET_HOST", fmt.Sprintf("http://127.0.0.1:%s", port)),
- DataDir: envOrDefault("DATA_DIR", defaultDataDir),
- DB: loadDB(),
- Redis: loadRedis(),
- }
+// appSpec is the environment-variable layout for core application settings.
+// Issuer carries no default tag because its fallback is derived from the
+// resolved Port at load time.
+type appSpec struct {
+ Identifier string `envconfig:"NAMESPACE" default:"esignet"`
+ Port int `envconfig:"PORT" default:"8088"`
+ Issuer string `envconfig:"MOSIP_ESIGNET_HOST"`
+ DataDir string `envconfig:"DATA_DIR" default:"./data"`
+ Provider string `envconfig:"AUTHN_PROVIDER" default:"mock"`
+ AuthFlowID string `envconfig:"AUTH_FLOW_ID" default:"flow-esignet"`
+ ThemeID string `envconfig:"THEME_ID" default:"theme-esignet"`
+ LayoutID string `envconfig:"LAYOUT_ID" default:"layout-esignet"`
+ EncryptionKey string `envconfig:"CRYPTO_ENCRYPTION_KEY" required:"true"`
}
-func envOrDefault(key, fallback string) string {
- if value := os.Getenv(key); value != "" {
- return value
+// LoadAppConfig loads the application configuration from the default data
+// directory and overlays environment-derived settings. It returns an error
+// (and a nil config) if the file cannot be read or any environment variable
+// cannot be parsed into its target type, so an invalid configuration fails
+// startup rather than being silently coerced or mistaken for a usable
+// zero-value struct.
+func LoadAppConfig() (*AppConfig, error) {
+ path := filepath.Join(defaultDataDir, appConfigFileName)
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", path, err)
+ }
+
+ expanded := os.ExpandEnv(string(data))
+ var cfg AppConfig
+ decoder := yaml.NewDecoder(strings.NewReader(expanded))
+ decoder.KnownFields(true)
+ if err := decoder.Decode(&cfg); err != nil {
+ return nil, fmt.Errorf("parse %s: %w", path, err)
}
- return fallback
+
+ if err := applyDefaults(&cfg); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
}
-func envInt(key string) int {
- raw := os.Getenv(key)
- if raw == "" {
- return 0
+func applyDefaults(cfg *AppConfig) error {
+ var s appSpec
+ if err := envconfig.Process("", &s); err != nil {
+ return fmt.Errorf("loading app config: %w", err)
}
- n, err := strconv.Atoi(raw)
+
+ cfg.Identifier = s.Identifier
+ cfg.Port = s.Port
+ cfg.Issuer = s.Issuer
+ if cfg.Issuer == "" {
+ cfg.Issuer = fmt.Sprintf("http://127.0.0.1:%d", cfg.Port)
+ }
+ cfg.DataDir = s.DataDir
+ yamlDB := cfg.DB
+ db, err := loadDB()
if err != nil {
- return 0
+ return err
+ }
+ cfg.DB = *db
+ if !hasDBEnvConfig() && yamlDB.DSN != "" {
+ cfg.DB.DSN = yamlDB.DSN
}
- return n
+ if yamlDB.Pool.MaxOpenConns > 0 {
+ cfg.DB.Pool = yamlDB.Pool
+ }
+ redisCfg, err := loadRedis()
+ if err != nil {
+ return err
+ }
+ cfg.Redis = *redisCfg
+ cfg.Provider = s.Provider
+ cfg.LayoutID = s.LayoutID
+ cfg.ThemeID = s.ThemeID
+ cfg.AuthFlowID = s.AuthFlowID
+
+ cfg.Server.Port = cfg.Port
+ cfg.Server.Identifier = cfg.Identifier
+ cfg.Server.PublicURL = cfg.Issuer
+ cfg.Server.HTTPOnly = strings.HasPrefix(cfg.Issuer, "http://")
+
+ cfg.JWT.Issuer = cfg.Issuer
+ cfg.JWT.Audience = cfg.Issuer
+ cfg.JWT.PreferredKeyID = "default-key"
+ cfg.JWT.ValidityPeriod = 3600
+
+ cfg.EncryptionConfig.Key = s.EncryptionKey
+
+ cfg.Cache.Disabled = false
+ cfg.Cache.Type = "redis"
+ cfg.Cache.Size = 1000
+ cfg.Cache.TTL = 3600
+ cfg.Cache.EvictionPolicy = "LRU"
+ cfg.Cache.Redis.Address = fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
+ cfg.Cache.Redis.Password = cfg.Redis.Password
+ cfg.Cache.Redis.DB = cfg.Redis.DB
+
+ cfg.Observability.Enabled = true
+
+ cfg.GateClient.Scheme = defaultGateScheme
+ cfg.GateClient.Hostname = defaultGateHostname
+ cfg.GateClient.Port = defaultGatePort
+ cfg.GateClient.LoginPath = defaultGateLoginPath
+ cfg.GateClient.ErrorPath = defaultGateErrorPath
+
+ cfg.Flow.DefaultAuthFlowHandle = "default"
+ cfg.Flow.UserOnboardingFlowHandle = "user-onboarding"
+ cfg.Flow.MaxVersionHistory = 1
+ cfg.Flow.AutoInferRegistration = false
+ cfg.Flow.Store = "memory"
+ cfg.Flow.Executors = []string{"CredentialsAuthExecutor", "AuthAssertExecutor", "ConsentExecutor"}
+ cfg.Flow.Interceptors = []string{}
+
+ cfg.Consent.Enabled = false
+
+ cfg.OAuth.RefreshToken.RenewOnGrant = false
+ cfg.OAuth.AuthorizationCode.ValidityPeriod = 3600
+ cfg.OAuth.PAR.ExpiresIn = 3600
+ cfg.OAuth.AuthClass.AcrAMR = map[string][]string{}
+ cfg.OAuth.AuthClass.Amrs = []string{}
+ cfg.OAuth.AllowWildcardRedirectURI = true
+
+ cfg.KeyConfig.CertFile = defaultSigningCertPath
+ cfg.KeyConfig.KeyFile = defaultSigningKeyPath
+ cfg.KeyConfig.ID = "default-key"
+
+ return nil
}
-func envBool(key string) bool {
- switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) {
- case "1", "true", "yes", "on":
- return true
- default:
- return false
+// overrideSpec is the environment-variable layout for optional overrides of
+// gate-client and OAuth lifetime settings. Zero values mean "not set" and
+// leave the corresponding default in place.
+type overrideSpec struct {
+ UIScheme string `envconfig:"OIDC_UI_SCHEME"`
+ UIHostname string `envconfig:"OIDC_UI_HOSTNAME"`
+ UIPort int `envconfig:"OIDC_UI_PORT"`
+ UILoginPath string `envconfig:"OIDC_UI_LOGIN_PATH"`
+ UIErrorPath string `envconfig:"OIDC_UI_ERROR_PATH"`
+
+ AuthCodeLifetimeSecs int64 `envconfig:"OAUTH_AUTH_CODE_LIFETIME_SECONDS"`
+ PARExpirySecs int64 `envconfig:"OAUTH_PAR_EXPIRY_SECONDS"`
+ AccessTokenLifetimeSecs int64 `envconfig:"OAUTH_ACCESS_TOKEN_LIFETIME_SECONDS"`
+}
+
+// ApplyEnvOverrides overlays environment and application settings onto cfg.
+// Env vars take precedence over values from app.yaml.
+func ApplyEnvOverrides(cfg *AppConfig) error {
+ var s overrideSpec
+ if err := envconfig.Process("", &s); err != nil {
+ return fmt.Errorf("loading env overrides: %w", err)
+ }
+
+ if s.UIScheme != "" {
+ cfg.GateClient.Scheme = s.UIScheme
+ }
+ if s.UIHostname != "" {
+ cfg.GateClient.Hostname = s.UIHostname
+ }
+ if s.UIPort != 0 {
+ if s.UIPort < 1 || s.UIPort > 65535 {
+ return errors.New("invalid OIDC_UI_PORT: port must be between 1 and 65535")
+ }
+ cfg.GateClient.Port = s.UIPort
+ }
+ if s.UILoginPath != "" {
+ cfg.GateClient.LoginPath = s.UILoginPath
+ }
+ if s.UIErrorPath != "" {
+ cfg.GateClient.ErrorPath = s.UIErrorPath
+ }
+
+ if s.AuthCodeLifetimeSecs > 0 {
+ cfg.OAuth.AuthorizationCode.ValidityPeriod = s.AuthCodeLifetimeSecs
+ }
+ if s.PARExpirySecs > 0 {
+ cfg.OAuth.PAR.ExpiresIn = s.PARExpirySecs
+ }
+ if s.AccessTokenLifetimeSecs > 0 {
+ cfg.JWT.ValidityPeriod = s.AccessTokenLifetimeSecs
}
+ return nil
}
diff --git a/esignet-service/internal/config/db.go b/esignet-service/internal/config/db.go
index f71f9afef..47e7a5b5a 100644
--- a/esignet-service/internal/config/db.go
+++ b/esignet-service/internal/config/db.go
@@ -8,43 +8,76 @@ import (
"strings"
"time"
+ "github.com/kelseyhightower/envconfig"
_ "github.com/lib/pq" // PostgreSQL driver for database/sql
)
-const (
- defaultDBMaxOpenConns = 25
- defaultDBMaxIdleConns = 5
- defaultDBConnMaxLifetime = 5 * time.Minute
- defaultDBConnMaxIdleTime = 1 * time.Minute
- dbPingTimeout = 5 * time.Second
-)
+const dbPingTimeout = 5 * time.Second
// DBPool holds connection pool tuning parameters.
type DBPool struct {
- MaxOpenConns int
- MaxIdleConns int
- ConnMaxLifetime time.Duration
- ConnMaxIdleTime time.Duration
+ MaxOpenConns int `yaml:"max_open_conns"`
+ MaxIdleConns int `yaml:"max_idle_conns"`
+ ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime"`
+ ConnMaxIdleTime time.Duration `yaml:"conn_max_idle_time"`
}
// DB holds Postgres connection settings.
type DB struct {
- DSN string
- Pool DBPool
+ DSN string `yaml:"dsn"`
+ Pool DBPool `yaml:"pool"`
+}
+
+func hasDBEnvConfig() bool {
+ return os.Getenv("DATABASE_URL") != "" ||
+ os.Getenv("DATABASE_HOST") != "" ||
+ os.Getenv("DATABASE_PORT") != "" ||
+ os.Getenv("DATABASE_NAME") != "" ||
+ os.Getenv("DATABASE_USERNAME") != "" ||
+ os.Getenv("DATABASE_PASSWORD") != "" ||
+ os.Getenv("DB_DBUSER_PASSWORD") != ""
+}
+
+// dbSpec is the environment-variable layout for Postgres settings. The
+// individual connection params are only used to build the DSN when DATABASE_URL
+// is empty, but always carry their defaults.
+type dbSpec struct {
+ URL string `envconfig:"DATABASE_URL"`
+ Host string `envconfig:"DATABASE_HOST" default:"localhost"`
+ Port string `envconfig:"DATABASE_PORT" default:"5455"`
+ Name string `envconfig:"DATABASE_NAME" default:"mosip_esignet"`
+ User string `envconfig:"DATABASE_USERNAME" default:"postgres"`
+ Password string `envconfig:"DATABASE_PASSWORD"`
+ // PasswordAlt is the MOSIP deployment's name for the DB password,
+ // used when DATABASE_PASSWORD is unset.
+ PasswordAlt string `envconfig:"DB_DBUSER_PASSWORD"`
+
+ MaxOpenConns int `envconfig:"DB_MAX_OPEN_CONNS" default:"25"`
+ MaxIdleConns int `envconfig:"DB_MAX_IDLE_CONNS" default:"5"`
+ ConnMaxLifetime int `envconfig:"DB_CONN_MAX_LIFETIME_SECS"` // 0 = no limit
+ ConnMaxIdleTime int `envconfig:"DB_CONN_MAX_IDLE_TIME_SECS" default:"60"`
}
// loadDB reads Postgres connection config and pool settings from the environment.
-// Accepts either POSTGRES_URL (full DSN) or individual vars:
-// POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD.
+// Accepts either DATABASE_URL (full DSN) or individual vars:
+// DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD.
//
// Pool tuning (all optional):
//
-// DB_MAX_OPEN_CONNS — default 25
-// DB_MAX_IDLE_CONNS — default 5
-// DB_CONN_MAX_LIFETIME_SECS — default 300
+// DB_MAX_OPEN_CONNS — default 25
+// DB_MAX_IDLE_CONNS — default 5
+// DB_CONN_MAX_LIFETIME_SECS — default 0 (no limit)
// DB_CONN_MAX_IDLE_TIME_SECS — default 60
-func loadDB() DB {
- dsn := os.Getenv("POSTGRES_URL")
+func loadDB() (*DB, error) {
+ var s dbSpec
+ if err := envconfig.Process("", &s); err != nil {
+ return nil, fmt.Errorf("loading database config: %w", err)
+ }
+ if s.Password == "" {
+ s.Password = s.PasswordAlt
+ }
+
+ dsn := s.URL
if dsn != "" &&
(strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://")) &&
!strings.Contains(strings.ToLower(dsn), "sslmode=") {
@@ -55,58 +88,30 @@ func loadDB() DB {
dsn += sep + "sslmode=disable"
}
if dsn == "" {
- host := envOrDefault("DATABASE_HOST", "localhost")
- port := envOrDefault("DATABASE_PORT", "5432")
- dbname := envOrDefault("DATABASE_NAME", "mosip_esignet")
- user := envOrDefault("DATABASE_USERNAME", "postgres")
- password := os.Getenv("DB_DBUSER_PASSWORD")
- if password != "" {
+ if s.Password != "" {
dsn = fmt.Sprintf(
"host=%s port=%s dbname=%s user=%s password=%s sslmode=disable",
- host, port, dbname, user, password,
+ s.Host, s.Port, s.Name, s.User, s.Password,
)
} else {
// Omit password= when unset — lib/pq mis-parses "password= sslmode=..."
// and falls back to SSL, which fails against local Docker Postgres.
dsn = fmt.Sprintf(
"host=%s port=%s dbname=%s user=%s sslmode=disable",
- host, port, dbname, user,
+ s.Host, s.Port, s.Name, s.User,
)
}
}
- maxOpen := envInt("DB_MAX_OPEN_CONNS")
- if maxOpen <= 0 {
- maxOpen = defaultDBMaxOpenConns
- }
- maxIdle := envInt("DB_MAX_IDLE_CONNS")
- if maxIdle <= 0 {
- maxIdle = defaultDBMaxIdleConns
- }
- lifetimeSecs := envInt("DB_CONN_MAX_LIFETIME_SECS")
- var lifetime time.Duration
- if lifetimeSecs > 0 {
- lifetime = time.Duration(lifetimeSecs) * time.Second
- } else {
- lifetime = defaultDBConnMaxLifetime
- }
- idleSecs := envInt("DB_CONN_MAX_IDLE_TIME_SECS")
- var idleTime time.Duration
- if idleSecs > 0 {
- idleTime = time.Duration(idleSecs) * time.Second
- } else {
- idleTime = defaultDBConnMaxIdleTime
- }
-
- return DB{
+ return &DB{
DSN: dsn,
Pool: DBPool{
- MaxOpenConns: maxOpen,
- MaxIdleConns: maxIdle,
- ConnMaxLifetime: lifetime,
- ConnMaxIdleTime: idleTime,
+ MaxOpenConns: s.MaxOpenConns,
+ MaxIdleConns: s.MaxIdleConns,
+ ConnMaxLifetime: time.Duration(s.ConnMaxLifetime) * time.Second,
+ ConnMaxIdleTime: time.Duration(s.ConnMaxIdleTime) * time.Second,
},
- }
+ }, nil
}
// Open opens, configures the pool, and pings the Postgres connection.
diff --git a/esignet-service/internal/config/redis.go b/esignet-service/internal/config/redis.go
index c66a6b995..9c57380c7 100644
--- a/esignet-service/internal/config/redis.go
+++ b/esignet-service/internal/config/redis.go
@@ -7,21 +7,11 @@ import (
"strings"
"time"
+ "github.com/kelseyhightower/envconfig"
"github.com/redis/go-redis/v9"
)
-const (
- defaultRedisHost = "localhost"
- defaultRedisPort = "6379"
- defaultRedisPoolSize = 10
- defaultRedisMinIdleConns = 2
- defaultRedisConnMaxIdleTime = 5 * time.Minute
- defaultRedisDialTimeout = 5 * time.Second
- defaultRedisReadTimeout = 3 * time.Second
- defaultRedisWriteTimeout = 3 * time.Second
- defaultRedisKeyPrefix = "esignet:"
- redisPingTimeout = 5 * time.Second
-)
+const redisPingTimeout = 5 * time.Second
// Redis holds all settings needed to open and configure a Redis client.
//
@@ -68,81 +58,73 @@ type Redis struct {
SentinelAddrs []string
}
-func loadRedis() Redis {
- poolSize := envInt("REDIS_POOL_SIZE")
- if poolSize <= 0 {
- poolSize = defaultRedisPoolSize
- }
- minIdle := envInt("REDIS_MIN_IDLE_CONNS")
- if minIdle <= 0 {
- minIdle = defaultRedisMinIdleConns
- }
-
- idleSecs := envInt("REDIS_CONN_MAX_IDLE_TIME_SECS")
- var idleTime time.Duration
- if idleSecs > 0 {
- idleTime = time.Duration(idleSecs) * time.Second
- } else {
- idleTime = defaultRedisConnMaxIdleTime
- }
-
- lifetimeSecs := envInt("REDIS_CONN_MAX_LIFETIME_SECS")
- lifetime := time.Duration(lifetimeSecs) * time.Second // 0 = no limit
-
- dialSecs := envInt("REDIS_DIAL_TIMEOUT_SECS")
- var dialTimeout time.Duration
- if dialSecs > 0 {
- dialTimeout = time.Duration(dialSecs) * time.Second
- } else {
- dialTimeout = defaultRedisDialTimeout
- }
+// redisSpec is the environment-variable layout for Redis settings.
+// SentinelAddrs is parsed as a raw string so the historical trim/drop-empty
+// splitting is preserved in loadRedis.
+type redisSpec struct {
+ URL string `envconfig:"REDIS_URL"`
+ Host string `envconfig:"REDIS_HOST" default:"localhost"`
+ Port string `envconfig:"REDIS_PORT" default:"6379"`
+ Password string `envconfig:"REDIS_PASSWORD"`
+ DB int `envconfig:"REDIS_DB"`
+ TLS bool `envconfig:"REDIS_TLS_ENABLED"`
+
+ PoolSize int `envconfig:"REDIS_POOL_SIZE" default:"10"`
+ MinIdleConns int `envconfig:"REDIS_MIN_IDLE_CONNS" default:"2"`
+ ConnMaxIdleTime int `envconfig:"REDIS_CONN_MAX_IDLE_TIME_SECS" default:"300"`
+ ConnMaxLifetime int `envconfig:"REDIS_CONN_MAX_LIFETIME_SECS"` // 0 = no limit
+ DialTimeout int `envconfig:"REDIS_DIAL_TIMEOUT_SECS" default:"5"`
+ ReadTimeout int `envconfig:"REDIS_READ_TIMEOUT_SECS" default:"3"`
+ WriteTimeout int `envconfig:"REDIS_WRITE_TIMEOUT_SECS" default:"3"`
+
+ KeyPrefix string `envconfig:"REDIS_KEY_PREFIX" default:"esignet:"`
+
+ SentinelMaster string `envconfig:"REDIS_SENTINEL_MASTER"`
+ SentinelAddrs string `envconfig:"REDIS_SENTINEL_ADDRS"`
+}
- readSecs := envInt("REDIS_READ_TIMEOUT_SECS")
- var readTimeout time.Duration
- if readSecs > 0 {
- readTimeout = time.Duration(readSecs) * time.Second
- } else {
- readTimeout = defaultRedisReadTimeout
+// loadRedis reads Redis settings from environment variables. It returns nil on
+// error so a failed load can never be mistaken for a usable zero-value config.
+func loadRedis() (*Redis, error) {
+ var s redisSpec
+ if err := envconfig.Process("", &s); err != nil {
+ return nil, fmt.Errorf("loading redis config: %w", err)
}
- writeSecs := envInt("REDIS_WRITE_TIMEOUT_SECS")
- var writeTimeout time.Duration
- if writeSecs > 0 {
- writeTimeout = time.Duration(writeSecs) * time.Second
- } else {
- writeTimeout = defaultRedisWriteTimeout
+ // A non-negative lifetime is required; 0 means "no limit". A negative value
+ // is rejected rather than silently clamped so a misconfiguration fails loudly.
+ if s.ConnMaxLifetime < 0 {
+ return nil, fmt.Errorf("loading redis config: REDIS_CONN_MAX_LIFETIME_SECS must not be negative, got %d", s.ConnMaxLifetime)
}
+ lifetime := time.Duration(s.ConnMaxLifetime) * time.Second // 0 = no limit
- keyPrefix := envOrDefault("REDIS_KEY_PREFIX", defaultRedisKeyPrefix)
-
- sentinelAddrsRaw := envOrDefault("REDIS_SENTINEL_ADDRS", "")
var sentinelAddrs []string
- if sentinelAddrsRaw != "" {
- for _, a := range strings.Split(sentinelAddrsRaw, ",") {
+ if s.SentinelAddrs != "" {
+ for _, a := range strings.Split(s.SentinelAddrs, ",") {
if a = strings.TrimSpace(a); a != "" {
sentinelAddrs = append(sentinelAddrs, a)
}
}
}
- return Redis{
- URL: envOrDefault("REDIS_URL", ""),
- Host: envOrDefault("REDIS_HOST", defaultRedisHost),
- Port: envOrDefault("REDIS_PORT", defaultRedisPort),
- Password: envOrDefault("REDIS_PASSWORD", ""),
- DB: envInt("REDIS_DB"),
- TLS: envBool("REDIS_TLS_ENABLED"),
- PoolSize: poolSize,
- MinIdleConns: minIdle,
- ConnMaxIdleTime: idleTime,
+ return &Redis{
+ URL: s.URL,
+ Host: s.Host,
+ Port: s.Port,
+ Password: s.Password,
+ DB: s.DB,
+ TLS: s.TLS,
+ PoolSize: s.PoolSize,
+ MinIdleConns: s.MinIdleConns,
+ ConnMaxIdleTime: time.Duration(s.ConnMaxIdleTime) * time.Second,
ConnMaxLifetime: lifetime,
- DialTimeout: dialTimeout,
- ReadTimeout: readTimeout,
- WriteTimeout: writeTimeout,
- KeyPrefix: keyPrefix,
- SentinelMaster: envOrDefault("REDIS_SENTINEL_MASTER", ""),
+ DialTimeout: time.Duration(s.DialTimeout) * time.Second,
+ ReadTimeout: time.Duration(s.ReadTimeout) * time.Second,
+ WriteTimeout: time.Duration(s.WriteTimeout) * time.Second,
+ KeyPrefix: s.KeyPrefix,
+ SentinelMaster: s.SentinelMaster,
SentinelAddrs: sentinelAddrs,
- }
+ }, nil
}
// Open creates a Redis client, applies pool/timeout settings, and pings the server.
diff --git a/esignet-service/internal/config/redis_test.go b/esignet-service/internal/config/redis_test.go
new file mode 100644
index 000000000..9b9ce4c5f
--- /dev/null
+++ b/esignet-service/internal/config/redis_test.go
@@ -0,0 +1,122 @@
+package config
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+// redisEnvKeys is every variable loadRedis reads; tests clear them all so a
+// stray value in the ambient environment cannot mask a default.
+var redisEnvKeys = []string{
+ "REDIS_URL", "REDIS_HOST", "REDIS_PORT", "REDIS_PASSWORD", "REDIS_DB",
+ "REDIS_TLS_ENABLED", "REDIS_POOL_SIZE", "REDIS_MIN_IDLE_CONNS",
+ "REDIS_CONN_MAX_IDLE_TIME_SECS", "REDIS_CONN_MAX_LIFETIME_SECS",
+ "REDIS_DIAL_TIMEOUT_SECS", "REDIS_READ_TIMEOUT_SECS", "REDIS_WRITE_TIMEOUT_SECS",
+ "REDIS_KEY_PREFIX", "REDIS_SENTINEL_MASTER", "REDIS_SENTINEL_ADDRS",
+}
+
+// clearRedisEnv unsets every Redis variable so envconfig's `default` tags apply.
+// t.Setenv first registers restoration of any ambient value at test cleanup;
+// os.Unsetenv then removes it for the duration of the test (an empty string
+// would count as "set" and suppress the default).
+func clearRedisEnv(t *testing.T) {
+ t.Helper()
+ for _, k := range redisEnvKeys {
+ t.Setenv(k, "")
+ require.NoError(t, os.Unsetenv(k))
+ }
+}
+
+func TestLoadRedis_defaults(t *testing.T) {
+ clearRedisEnv(t)
+
+ cfg, err := loadRedis()
+ require.NoError(t, err)
+ require.Empty(t, cfg.URL)
+ require.Equal(t, "localhost", cfg.Host)
+ require.Equal(t, "6379", cfg.Port)
+ require.Empty(t, cfg.Password)
+ require.Equal(t, 0, cfg.DB)
+ require.False(t, cfg.TLS)
+ require.Equal(t, 10, cfg.PoolSize)
+ require.Equal(t, 2, cfg.MinIdleConns)
+ require.Equal(t, 5*time.Minute, cfg.ConnMaxIdleTime)
+ require.Equal(t, time.Duration(0), cfg.ConnMaxLifetime) // 0 = no limit
+ require.Equal(t, 5*time.Second, cfg.DialTimeout)
+ require.Equal(t, 3*time.Second, cfg.ReadTimeout)
+ require.Equal(t, 3*time.Second, cfg.WriteTimeout)
+ require.Equal(t, "esignet:", cfg.KeyPrefix)
+ require.Empty(t, cfg.SentinelMaster)
+ require.Nil(t, cfg.SentinelAddrs)
+}
+
+func TestLoadRedis_overrides(t *testing.T) {
+ clearRedisEnv(t)
+ t.Setenv("REDIS_URL", "redis://:secret@cache:6380/2")
+ t.Setenv("REDIS_HOST", "cache")
+ t.Setenv("REDIS_PORT", "6380")
+ t.Setenv("REDIS_PASSWORD", "secret")
+ t.Setenv("REDIS_DB", "2")
+ t.Setenv("REDIS_TLS_ENABLED", "true")
+ t.Setenv("REDIS_POOL_SIZE", "20")
+ t.Setenv("REDIS_MIN_IDLE_CONNS", "4")
+ t.Setenv("REDIS_CONN_MAX_IDLE_TIME_SECS", "120")
+ t.Setenv("REDIS_CONN_MAX_LIFETIME_SECS", "600")
+ t.Setenv("REDIS_DIAL_TIMEOUT_SECS", "7")
+ t.Setenv("REDIS_READ_TIMEOUT_SECS", "8")
+ t.Setenv("REDIS_WRITE_TIMEOUT_SECS", "9")
+ t.Setenv("REDIS_KEY_PREFIX", "test:")
+ t.Setenv("REDIS_SENTINEL_MASTER", "mymaster")
+
+ cfg, err := loadRedis()
+ require.NoError(t, err)
+ require.Equal(t, "redis://:secret@cache:6380/2", cfg.URL)
+ require.Equal(t, "cache", cfg.Host)
+ require.Equal(t, "6380", cfg.Port)
+ require.Equal(t, "secret", cfg.Password)
+ require.Equal(t, 2, cfg.DB)
+ require.True(t, cfg.TLS)
+ require.Equal(t, 20, cfg.PoolSize)
+ require.Equal(t, 4, cfg.MinIdleConns)
+ // Each *_SECS value lands on its own distinct field — guards against a
+ // swapped read/write/dial timeout mapping.
+ require.Equal(t, 120*time.Second, cfg.ConnMaxIdleTime)
+ require.Equal(t, 600*time.Second, cfg.ConnMaxLifetime)
+ require.Equal(t, 7*time.Second, cfg.DialTimeout)
+ require.Equal(t, 8*time.Second, cfg.ReadTimeout)
+ require.Equal(t, 9*time.Second, cfg.WriteTimeout)
+ require.Equal(t, "test:", cfg.KeyPrefix)
+ require.Equal(t, "mymaster", cfg.SentinelMaster)
+}
+
+func TestLoadRedis_sentinelAddrsTrimAndDropEmpty(t *testing.T) {
+ clearRedisEnv(t)
+ t.Setenv("REDIS_SENTINEL_ADDRS", "a:26379, b:26379 ,, c:26379")
+
+ cfg, err := loadRedis()
+ require.NoError(t, err)
+ require.Equal(t, []string{"a:26379", "b:26379", "c:26379"}, cfg.SentinelAddrs)
+}
+
+// An unparseable numeric value now fails loudly rather than silently falling
+// back to a default.
+func TestLoadRedis_invalidNumberReturnsError(t *testing.T) {
+ clearRedisEnv(t)
+ t.Setenv("REDIS_POOL_SIZE", "not-a-number")
+
+ _, err := loadRedis()
+ require.Error(t, err)
+}
+
+// A negative max-lifetime parses as a valid integer but is not a meaningful
+// duration, so loadRedis now rejects it rather than silently clamping to 0.
+func TestLoadRedis_negativeLifetimeReturnsError(t *testing.T) {
+ clearRedisEnv(t)
+ t.Setenv("REDIS_CONN_MAX_LIFETIME_SECS", "-1")
+
+ _, err := loadRedis()
+ require.Error(t, err)
+}
diff --git a/esignet-service/internal/engine/actor_provider.go b/esignet-service/internal/engine/actor_provider.go
new file mode 100644
index 000000000..bba4658f7
--- /dev/null
+++ b/esignet-service/internal/engine/actor_provider.go
@@ -0,0 +1,162 @@
+// Package engine provides ThunderID engine host integrations for the embedder.
+package engine
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/clientmgmt"
+ "github.com/mosip/esignet/internal/config"
+ "github.com/mosip/esignet/internal/engine/shared"
+)
+
+type actorProvider struct {
+ clientSvc *clientmgmt.Service
+ config *config.AppConfig
+}
+
+// NewActorProvider returns a minimal host.ActorProvider stub. Declarative resources
+// supply applications, flows, and related SoR data to the engine directly.
+func NewActorProvider(clientSvc *clientmgmt.Service, config *config.AppConfig) providers.ActorProvider {
+ return &actorProvider{clientSvc: clientSvc, config: config}
+}
+
+func (p *actorProvider) GetOAuthClientByClientID(
+ ctx context.Context, clientID string,
+) (*providers.OAuthClient, *common.ServiceError) {
+ client, err := p.clientSvc.GetClient(ctx, clientID)
+ if err != nil {
+ return nil, shared.ClientNotFoundError
+ }
+ return &providers.OAuthClient{
+ ID: client.ClientID,
+ OUID: client.RpID,
+ ClientID: client.ClientID,
+ GrantTypes: []providers.GrantType{providers.GrantTypeAuthorizationCode},
+ RedirectURIs: client.RedirectURIs,
+ ResponseTypes: []providers.ResponseType{providers.ResponseTypeCode},
+ TokenEndpointAuthMethod: providers.TokenEndpointAuthMethodPrivateKeyJWT,
+ PKCERequired: false,
+ PublicClient: false,
+ Certificate: &providers.Certificate{
+ Type: "JWKS",
+ Value: getJWKS(client.PublicKey),
+ },
+ }, nil
+}
+
+func (p *actorProvider) GetOAuthProfileByID(
+ ctx context.Context, id string,
+) (*providers.OAuthProfile, *common.ServiceError) {
+ client, err := p.clientSvc.GetClient(ctx, id)
+ if err != nil {
+ return nil, shared.ClientNotFoundError
+ }
+ return &providers.OAuthProfile{
+ RedirectURIs: client.RedirectURIs,
+ GrantTypes: client.GrantTypes,
+ ResponseTypes: []string{string(providers.ResponseTypeCode)},
+ TokenEndpointAuthMethod: string(providers.TokenEndpointAuthMethodPrivateKeyJWT),
+ PKCERequired: true,
+ PublicClient: false,
+ RequirePushedAuthorizationRequests: true,
+ DPoPBoundAccessTokens: true,
+ IncludeActClaim: true,
+ Token: &providers.OAuthTokenConfig{
+ AccessToken: &providers.AccessTokenConfig{
+ ValidityPeriod: 5 * 60,
+ UserAttributes: client.Claims,
+ },
+ IDToken: &providers.IDTokenConfig{
+ ValidityPeriod: 5 * 60,
+ UserAttributes: []string{},
+ },
+ },
+ Scopes: []string{"openid"},
+ UserInfo: &providers.UserInfoConfig{
+ ResponseType: providers.UserInfoResponseTypeJSON,
+ UserAttributes: client.Claims,
+ },
+ ScopeClaims: map[string][]string{
+ "openid": {"sub"},
+ },
+ Certificate: &providers.Certificate{
+ Type: "JWKS",
+ Value: getJWKS(client.PublicKey),
+ },
+ AcrValues: client.AcrValues,
+ }, nil
+}
+
+func (p *actorProvider) GetInboundClientByID(
+ ctx context.Context, id string,
+) (*providers.InboundClient, *common.ServiceError) {
+ client, err := p.clientSvc.GetClient(ctx, id)
+ if err != nil {
+ return nil, shared.ClientNotFoundError
+ }
+
+ properties := make(map[string]interface{})
+ properties["name"] = client.ClientName
+ properties["description"] = client.ClientName
+
+ return &providers.InboundClient{
+ ID: client.ClientID,
+ AuthFlowID: p.config.AuthFlowID,
+ IsRegistrationFlowEnabled: false,
+ IsRecoveryFlowEnabled: false,
+ ThemeID: p.config.ThemeID,
+ LayoutID: p.config.LayoutID,
+ Assertion: &providers.AssertionConfig{
+ ValidityPeriod: 5 * 60, // 5 minutes
+ UserAttributes: client.Claims,
+ },
+ LoginConsent: &providers.LoginConsentConfig{
+ ValidityPeriod: configInt64(client.AdditionalConfig, "consent_expire_in_mins", 0),
+ },
+ Properties: properties,
+ IsReadOnly: false,
+ }, nil
+}
+
+func (p *actorProvider) GetActor(_ string) (*providers.Entity, *common.ServiceError) {
+ return nil, nil
+}
+
+func (p *actorProvider) GetActorGroups(_ string) ([]providers.EntityGroup, *common.ServiceError) {
+ return nil, nil
+}
+
+func getJWKS(publicKey string) string {
+ var parsed map[string]json.RawMessage
+ if err := json.Unmarshal([]byte(publicKey), &parsed); err == nil {
+ if _, hasKeys := parsed["keys"]; !hasKeys {
+ publicKey = fmt.Sprintf(`{"keys":[%s]}`, publicKey)
+ }
+ }
+ return publicKey
+}
+
+func configInt64(cfg map[string]any, key string, defaultValue int64) int64 {
+ if cfg == nil {
+ return defaultValue
+ }
+ v, ok := cfg[key]
+ if !ok {
+ return defaultValue
+ }
+ switch n := v.(type) {
+ case int64:
+ return n
+ case int:
+ return int64(n)
+ case float64:
+ return int64(n)
+ default:
+ return defaultValue
+ }
+}
diff --git a/esignet-service/internal/engine/actors.go b/esignet-service/internal/engine/actors.go
deleted file mode 100644
index 38fbcfe3e..000000000
--- a/esignet-service/internal/engine/actors.go
+++ /dev/null
@@ -1,154 +0,0 @@
-// Package engine provides ThunderID engine host integrations for the embedder.
-package engine
-
-import (
- "context"
- "encoding/json"
- "fmt"
-
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
- "github.com/thunder-id/thunderid/pkg/thunderidengine/runtime"
-
- "github.com/mosip/esignet/internal/clientmgmt"
-)
-
-type actorProvider struct {
- clientSvc *clientmgmt.Service
- config Config
-}
-
-// NewActorProvider returns a minimal host.ActorProvider stub. Declarative resources
-// supply applications, flows, and related SoR data to the engine directly.
-func NewActorProvider(clientSvc *clientmgmt.Service, config Config) host.ActorProvider {
- return &actorProvider{clientSvc: clientSvc, config: config}
-}
-
-func (*actorProvider) IdentifyEntity(map[string]any) (*string, error) {
- return nil, runtime.ErrNotFound
-}
-
-func (p *actorProvider) GetEntity(entityID string) (*host.Actor, error) {
- client, err := p.clientSvc.GetClient(context.Background(), entityID)
- if err != nil {
- return nil, err
- }
-
- systemAttributes := map[string]any{
- "name": client.ClientName,
- "description": client.ClientName,
- }
- systemAttributesBytes, err := json.Marshal(systemAttributes)
- if err != nil {
- return nil, err
- }
- return &host.Actor{
- ID: client.ClientID,
- EntityType: "client",
- OUID: client.RpID,
- SystemAttributes: systemAttributesBytes,
- }, nil
-}
-
-func (*actorProvider) SearchEntities(map[string]any) ([]*host.Actor, error) {
- return nil, nil
-}
-
-func (p *actorProvider) GetApplication(ctx context.Context, appID string) (*host.Application, error) {
- client, err := p.clientSvc.GetClient(ctx, appID)
- if err != nil {
- return nil, err
- }
- return &host.Application{
- ID: client.RpID,
- Name: client.ClientName,
- OUID: client.RpID,
- EntityID: client.ClientID,
- ThemeID: p.config.ThemeID,
- LayoutID: p.config.LayoutID,
- }, nil
-}
-
-func (p *actorProvider) GetInboundClientByEntityID(ctx context.Context, entityID string) (*host.InboundClient, error) {
- client, err := p.clientSvc.GetClient(ctx, entityID)
- if err != nil {
- return nil, err
- }
- return inboundClientFromDB(client, p.config), nil
-}
-
-func (p *actorProvider) GetInboundClientByClientID(ctx context.Context, clientID string) (*host.InboundClient, error) {
- client, err := p.clientSvc.GetClient(ctx, clientID)
- if err != nil {
- return nil, err
- }
- return inboundClientFromDB(client, p.config), nil
-}
-
-func inboundClientFromDB(client clientmgmt.ClientResponse, cfg Config) *host.InboundClient {
- tokenAuthMethod := "private_key_jwt"
- if len(client.AuthMethods) > 0 {
- tokenAuthMethod = client.AuthMethods[0]
- }
- pkceRequired := configBool(client.AdditionalConfig, "require_pkce") ||
- configBool(client.AdditionalConfig, "pkce_required")
-
- var certificate *host.Certificate
- if client.PublicKey != "" {
- certValue := client.PublicKey
- var parsed map[string]json.RawMessage
- if err := json.Unmarshal([]byte(certValue), &parsed); err == nil {
- if _, hasKeys := parsed["keys"]; !hasKeys {
- certValue = fmt.Sprintf(`{"keys":[%s]}`, certValue)
- }
- }
- certificate = &host.Certificate{
- Type: "JWKS",
- Value: certValue,
- }
- }
-
- return &host.InboundClient{
- ClientID: client.ClientID,
- EntityID: client.ClientID,
- ApplicationID: client.RpID,
- OUID: client.RpID,
- LogoURL: client.LogoURI,
- GrantTypes: client.GrantTypes,
- RedirectURIs: client.RedirectURIs,
- ResponseTypes: []string{"code"},
- TokenEndpointAuthMethod: tokenAuthMethod,
- PKCERequired: pkceRequired,
- PublicClient: false,
- Certificate: certificate,
- AuthFlowID: cfg.AuthFlowID,
- RegistrationFlowID: cfg.RegistrationFlowID,
- IsRegistrationFlowEnabled: false,
- RecoveryFlowID: cfg.RecoveryFlowID,
- IsRecoveryFlowEnabled: false,
- }
-}
-
-func configBool(cfg map[string]any, key string) bool {
- if cfg == nil {
- return false
- }
- v, ok := cfg[key]
- if !ok {
- return false
- }
- switch b := v.(type) {
- case bool:
- return b
- case string:
- return b == "true"
- default:
- return false
- }
-}
-
-func (p *actorProvider) GetEntityType(_ context.Context, typeID string) (*host.EntityType, error) {
- return &host.EntityType{
- ID: typeID,
- Name: "application",
- }, nil
-}
diff --git a/esignet-service/internal/engine/authn_factory.go b/esignet-service/internal/engine/authn_factory.go
index 7e115a9ca..d6ee85740 100644
--- a/esignet-service/internal/engine/authn_factory.go
+++ b/esignet-service/internal/engine/authn_factory.go
@@ -3,28 +3,26 @@ package engine
import (
"fmt"
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
"github.com/mosip/esignet/internal/clientmgmt"
+ "github.com/mosip/esignet/internal/config"
"github.com/mosip/esignet/internal/engine/mock"
"github.com/mosip/esignet/internal/engine/mosip"
"github.com/mosip/esignet/internal/engine/sunbird"
)
-// NewAuthnProviderFromConfig creates a host.AuthnProvider based on configuration.
-func NewAuthnProviderFromConfig(provider string, clientSvc *clientmgmt.Service) (host.AuthnProvider, error) {
-
+// NewAuthnProvider creates a providers.AuthnProviderManager based on configuration.
+func NewAuthnProvider(provider string, appConfig *config.AppConfig, clientSvc *clientmgmt.Service) (providers.AuthnProviderManager, error) {
switch provider {
case "mosip":
- cfg := mosip.LoadConfig()
- authnProvider, err := mosip.NewMosipAuthnProvider(cfg, clientSvc)
+ authnProvider, err := mosip.NewMosipAuthnProvider(appConfig, clientSvc)
if err != nil {
return nil, err
}
return authnProvider, nil
case "sunbird":
- cfg := sunbird.LoadConfig()
- authnProvider, err := sunbird.NewSunbirdAuthnProvider(cfg)
+ authnProvider, err := sunbird.NewSunbirdAuthnProvider()
if err != nil {
return nil, err
}
diff --git a/esignet-service/internal/engine/authz.go b/esignet-service/internal/engine/authz.go
deleted file mode 100644
index 71016587e..000000000
--- a/esignet-service/internal/engine/authz.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package engine
-
-import (
- "context"
- "log"
-
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
-)
-
-type roleProvider struct{}
-
-// NewRoleProvider returns a permissive role provider for local development.
-func NewRoleProvider() host.RoleProvider {
- return &roleProvider{}
-}
-
-func (*roleProvider) GetAuthorizedPermissions(
- _ context.Context, entityID string, groups []string, requestedPermissions []string,
-) ([]string, error) {
- log.Println("GetAuthorizedPermissions", entityID, groups, requestedPermissions)
- return append([]string(nil), requestedPermissions...), nil
-}
-
-func (*roleProvider) GetUserRoles(_ context.Context, entityID string, groupIDs []string) ([]string, error) {
- log.Println("GetUserRoles", entityID, groupIDs)
- return []string{}, nil
-}
diff --git a/esignet-service/internal/engine/authz_provider.go b/esignet-service/internal/engine/authz_provider.go
new file mode 100644
index 000000000..f9b6a3c77
--- /dev/null
+++ b/esignet-service/internal/engine/authz_provider.go
@@ -0,0 +1,42 @@
+package engine
+
+import (
+ "context"
+
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/config"
+)
+
+type authorizationProvider struct {
+ cfg *config.AppConfig
+}
+
+// NewAuthorizationProvider returns a permissive authorization provider for local development.
+func NewAuthorizationProvider(cfg *config.AppConfig) providers.AuthorizationProvider {
+ return &authorizationProvider{cfg: cfg}
+}
+
+func (*authorizationProvider) EvaluateAccess(
+ _ context.Context,
+ request providers.AccessEvaluationRequest,
+) (*providers.AccessEvaluationResponse, *common.ServiceError) {
+ return &providers.AccessEvaluationResponse{
+ Decision: true,
+ Context: request.Context,
+ }, nil
+}
+
+func (*authorizationProvider) EvaluateAccessBatch(
+ _ context.Context,
+ _ providers.AccessEvaluationsRequest,
+) (*providers.AccessEvaluationsResponse, *common.ServiceError) {
+ return &providers.AccessEvaluationsResponse{
+ Evaluations: []providers.AccessEvaluationResponse{
+ {
+ Decision: true,
+ },
+ },
+ }, nil
+}
diff --git a/esignet-service/internal/engine/config.go b/esignet-service/internal/engine/config.go
deleted file mode 100644
index 4f4d834e4..000000000
--- a/esignet-service/internal/engine/config.go
+++ /dev/null
@@ -1,170 +0,0 @@
-package engine
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strconv"
- "strings"
-
- "github.com/thunder-id/thunderid/pkg/thunderidengine"
-
- "github.com/mosip/esignet/internal/config"
-)
-
-// Config holds host-layer settings for auth providers and declarative defaults.
-type Config struct {
- Provider string
- AuthFlowID string
- RegistrationFlowID string
- RecoveryFlowID string
- ThemeID string
- LayoutID string
-}
-
-// LoadConfig reads host-layer settings from the environment.
-func LoadConfig() Config {
- return Config{
- Provider: os.Getenv("AUTHN_PROVIDER"),
- ThemeID: os.Getenv("DEFAULT_THEME_ID"),
- LayoutID: os.Getenv("DEFAULT_LAYOUT_ID"),
- AuthFlowID: os.Getenv("DEFAULT_AUTH_FLOW_ID"),
- RegistrationFlowID: os.Getenv("DEFAULT_REGISTRATION_FLOW_ID"),
- RecoveryFlowID: os.Getenv("DEFAULT_RECOVERY_FLOW_ID"),
- }
-}
-
-const (
- defaultGatePort = "3000"
- defaultSigningKeyPath = "./keys/signing.key"
- defaultSigningCertPath = "./keys/signing.crt"
- defaultGateScheme = "http"
- defaultGateHostname = "127.0.0.1"
- defaultGateLoginPath = "/signin"
- defaultGateErrorPath = "/error"
-)
-
-var enabledExecutors = []string{
- "CredentialsAuthExecutor",
- "AuthorizationExecutor",
- "AuthAssertExecutor",
- "ConsentExecutor",
-}
-
-// BuildThunderConfig merges application env settings into the Thunder engine config.
-func BuildThunderConfig(appCfg config.AppConfig) (*thunderidengine.Config, []string, error) {
- dataDir := appCfg.DataDir
- cfg, err := thunderidengine.LoadEngineConfig(dataDir)
- if err != nil {
- return nil, nil, err
- }
-
- if encKey := os.Getenv("CRYPTO_ENCRYPTION_KEY"); encKey != "" {
- cfg.Crypto.Encryption.Key = encKey
- } else {
- cfg.Crypto.Encryption.Key = os.ExpandEnv(cfg.Crypto.Encryption.Key)
- }
- if cfg.Crypto.Encryption.Key == "" {
- return nil, nil, fmt.Errorf("CRYPTO_ENCRYPTION_KEY must be set")
- }
-
- cfg.OAuth.RefreshToken.RenewOnGrant = false
-
- cfg.DeclarativeResources.Enabled = true
- cfg.Flow.Store = "declarative"
- cfg.Resource.Store = "declarative"
- cfg.OrganizationUnit.Store = "declarative"
- cfg.IdentityProvider.Store = "declarative"
- cfg.Role.Store = "declarative"
- cfg.Theme.Store = "declarative"
- cfg.Layout.Store = "declarative"
- cfg.Translation.Store = "declarative"
-
- issuer := appCfg.Issuer
- audience := envOrDefault("JWT_AUDIENCE", "application")
- preferredKeyID := envOrDefault("JWT_PREFERRED_KEY_ID", "default-key")
- gateClientScheme := envOrDefault("OIDC_UI_SCHEME", defaultGateScheme)
- gateClientHostname := envOrDefault("OIDC_UI_HOSTNAME", defaultGateHostname)
- gateClientPort := envOrDefault("OIDC_UI_PORT", defaultGatePort)
- gateClientLoginPath := envOrDefault("OIDC_UI_LOGIN_PATH", defaultGateLoginPath)
- gateClientErrorPath := envOrDefault("OIDC_UI_ERROR_PATH", defaultGateErrorPath)
-
- cfg.JWT.Issuer = issuer
- cfg.JWT.Audience = audience
- cfg.JWT.PreferredKeyID = preferredKeyID
- cfg.Server.PublicURL = strings.TrimRight(issuer, "/")
- cfg.Server.HTTPOnly = strings.HasPrefix(issuer, "http://")
- cfg.GateClient.Scheme = gateClientScheme
- cfg.GateClient.Hostname = gateClientHostname
- gateClientPortInt, err := strconv.Atoi(gateClientPort)
- if err != nil {
- return nil, nil, fmt.Errorf("invalid OIDC_UI_PORT: %w", err)
- }
- if gateClientPortInt < 1 || gateClientPortInt > 65535 {
- return nil, nil, fmt.Errorf("invalid OIDC_UI_PORT: port must be between 1 and 65535")
- }
- cfg.GateClient.Port = gateClientPortInt
- cfg.GateClient.LoginPath = gateClientLoginPath
- cfg.GateClient.ErrorPath = gateClientErrorPath
-
- authorizationCodeLifetimeSeconds := envOrDefault("OAUTH_AUTH_CODE_LIFETIME_SECONDS", "120")
- authorizationCodeLifetimeSecondsInt, err := strconv.Atoi(authorizationCodeLifetimeSeconds)
- if err != nil {
- return nil, nil, fmt.Errorf("invalid OAUTH_AUTH_CODE_LIFETIME_SECONDS: %w", err)
- }
- if authorizationCodeLifetimeSecondsInt > 0 {
- cfg.OAuth.AuthorizationCode.ValidityPeriod = int64(authorizationCodeLifetimeSecondsInt)
- }
-
- parexpirySeconds := envOrDefault("OAUTH_PAR_EXPIRY_SECONDS", "3600")
- parexpirySecondsInt, err := strconv.Atoi(parexpirySeconds)
- if err != nil {
- return nil, nil, fmt.Errorf("invalid OAUTH_PAR_EXPIRY_SECONDS: %w", err)
- }
- if parexpirySecondsInt > 0 {
- cfg.OAuth.PAR.ExpiresIn = int64(parexpirySecondsInt)
- }
-
- accessTokenLifetimeSeconds := envOrDefault("OAUTH_ACCESS_TOKEN_LIFETIME_SECONDS", "3600")
- accessTokenLifetimeSecondsInt, err := strconv.Atoi(accessTokenLifetimeSeconds)
- if err != nil {
- return nil, nil, fmt.Errorf("invalid OAUTH_ACCESS_TOKEN_LIFETIME_SECONDS: %w", err)
- }
- if accessTokenLifetimeSecondsInt > 0 {
- cfg.JWT.ValidityPeriod = int64(accessTokenLifetimeSecondsInt)
- }
-
- return cfg, enabledExecutors, nil
-}
-
-// PKIPaths returns signing cert and key paths relative to the data directory.
-func PKIPaths(dataDir string) (certFile, keyFile string, err error) {
- dataDirAbs, err := filepath.Abs(dataDir)
- if err != nil {
- return "", "", fmt.Errorf("data dir: %w", err)
- }
- certAbs, err := filepath.Abs(defaultSigningCertPath)
- if err != nil {
- return "", "", fmt.Errorf("signing cert path: %w", err)
- }
- keyAbs, err := filepath.Abs(defaultSigningKeyPath)
- if err != nil {
- return "", "", fmt.Errorf("signing key path: %w", err)
- }
- certRel, err := filepath.Rel(dataDirAbs, certAbs)
- if err != nil {
- return "", "", fmt.Errorf("signing cert path: %w", err)
- }
- keyRel, err := filepath.Rel(dataDirAbs, keyAbs)
- if err != nil {
- return "", "", fmt.Errorf("signing key path: %w", err)
- }
- return certRel, keyRel, nil
-}
-
-func envOrDefault(key, fallback string) string {
- if value := os.Getenv(key); value != "" {
- return value
- }
- return fallback
-}
diff --git a/esignet-service/internal/engine/consent.go b/esignet-service/internal/engine/consent.go
deleted file mode 100644
index b74eb54f5..000000000
--- a/esignet-service/internal/engine/consent.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package engine
-
-import (
- "context"
-
- "github.com/thunder-id/thunderid/pkg/thunderidengine"
-)
-
-type consentEnforcer struct{}
-
-// NewConsentEnforcer returns a no-op consent enforcer for local development.
-func NewConsentEnforcer() thunderidengine.ConsentEnforcer {
- return &consentEnforcer{}
-}
-
-func (*consentEnforcer) ResolveConsent(
- _ context.Context, ouID, appID, appName, userID string,
- essentialAttributes, optionalAttributes, authorizedPermissions []string,
- availableAttributes *thunderidengine.AttributesResponse, forceReprompt bool,
- runtimeMetadata map[string]string,
-) (*thunderidengine.ConsentPromptData, *thunderidengine.ServiceError) {
- _ = ouID
- _ = appID
- _ = appName
- _ = userID
- _ = essentialAttributes
- _ = optionalAttributes
- _ = authorizedPermissions
- _ = availableAttributes
- _ = forceReprompt
- _ = runtimeMetadata
- return nil, nil
-}
-
-func (*consentEnforcer) RecordConsent(
- _ context.Context, ouID, appID, userID string,
- decisions *thunderidengine.ConsentDecisions, sessionToken string, validityPeriod int64,
- runtimeMetadata map[string]string,
-) (*thunderidengine.ConsentRecord, *thunderidengine.ServiceError) {
- _ = ouID
- _ = appID
- _ = userID
- _ = decisions
- _ = sessionToken
- _ = validityPeriod
- _ = runtimeMetadata
- return nil, nil
-}
diff --git a/esignet-service/internal/engine/consent_provider.go b/esignet-service/internal/engine/consent_provider.go
new file mode 100644
index 000000000..a6ccab233
--- /dev/null
+++ b/esignet-service/internal/engine/consent_provider.go
@@ -0,0 +1,34 @@
+package engine
+
+import (
+ "context"
+
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+)
+
+type consentEnforcer struct{}
+
+// NewConsentEnforcer returns a no-op consent enforcer for local development.
+func NewConsentEnforcer() providers.ConsentProvider {
+ return &consentEnforcer{}
+}
+
+func (*consentEnforcer) ResolveConsent(_ context.Context, _, _, _, _ string,
+ _, _, _ []string,
+ _ *providers.AttributesResponse, _ bool,
+ _ map[string]string) (
+ *providers.ConsentPromptData, *common.ServiceError) {
+
+ return nil, nil
+}
+
+// RecordConsent records the user's consent decisions and returns the persisted consent record.
+// If the user denied any essential attribute, ErrorEssentialConsentDenied is returned.
+func (*consentEnforcer) RecordConsent(_ context.Context, _, _, _ string,
+ _ *providers.ConsentDecisions, _ string, _ int64,
+ _ map[string]string) (
+ *providers.Consent, *common.ServiceError) {
+
+ return nil, nil
+}
diff --git a/esignet-service/internal/engine/design_provider.go b/esignet-service/internal/engine/design_provider.go
new file mode 100644
index 000000000..c79a87755
--- /dev/null
+++ b/esignet-service/internal/engine/design_provider.go
@@ -0,0 +1,174 @@
+package engine
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/stretchr/testify/assert/yaml"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/config"
+ "github.com/mosip/esignet/internal/engine/shared"
+)
+
+// layoutRequestWithID represents the request structure for creating a layout from file-based config.
+type layoutRequestWithID struct {
+ ID string `yaml:"id"`
+ Handle string `yaml:"handle"`
+ DisplayName string `yaml:"displayName"`
+ Description string `yaml:"description,omitempty"`
+ Layout interface{} `yaml:"layout"`
+}
+
+// themeRequestWithID represents the request structure for creating a theme from file-based config.
+type themeRequestWithID struct {
+ ID string `yaml:"id"`
+ Handle string `yaml:"handle"`
+ DisplayName string `yaml:"displayName"`
+ Description string `yaml:"description,omitempty"`
+ Theme interface{} `yaml:"theme"`
+}
+
+// Layout represents a layout configuration.
+type Layout struct {
+ ID string `json:"id" yaml:"id,omitempty"`
+ Handle string `json:"handle" yaml:"handle"`
+ DisplayName string `json:"displayName" yaml:"displayName"`
+ Description string `json:"description,omitempty" yaml:"description,omitempty"`
+ Layout json.RawMessage `json:"layout" yaml:"layout"`
+ CreatedAt string `json:"createdAt" yaml:"createdAt,omitempty"`
+ UpdatedAt string `json:"updatedAt" yaml:"updatedAt,omitempty"`
+ IsReadOnly bool `json:"isReadOnly" yaml:"isReadOnly"`
+}
+
+// Theme represents a theme configuration.
+type Theme struct {
+ ID string `json:"id" yaml:"id,omitempty"`
+ Handle string `json:"handle" yaml:"handle"`
+ DisplayName string `json:"displayName" yaml:"displayName"`
+ Description string `json:"description" yaml:"description,omitempty"`
+ Theme json.RawMessage `json:"theme" yaml:"theme"`
+ CreatedAt string `json:"createdAt" yaml:"createdAt,omitempty"`
+ UpdatedAt string `json:"updatedAt" yaml:"updatedAt,omitempty"`
+ IsReadOnly bool `json:"isReadOnly" yaml:"isReadOnly"`
+}
+
+type designProvider struct {
+ cfg *config.AppConfig
+}
+
+// NewDesignProvider returns a file-based design provider backed by the configured data directory.
+func NewDesignProvider(cfg *config.AppConfig) providers.DesignProvider {
+ return &designProvider{cfg: cfg}
+}
+
+func (p *designProvider) ResolveDesign(
+ _ context.Context, _ providers.DesignResolveType, _ string,
+) (*providers.DesignResponse, *common.ServiceError) {
+
+ if p.cfg.LayoutID == "" || p.cfg.ThemeID == "" {
+ return nil, shared.InvalidRequestError
+ }
+
+ layoutData, err := os.ReadFile(filepath.Join(p.cfg.DataDir, "layouts", p.cfg.LayoutID+".yaml"))
+ if err != nil {
+ return nil, shared.FileNotFoundError
+ }
+ layout, err := parseToLayout(layoutData)
+ if err != nil {
+ return nil, shared.FileUnmarshallError
+ }
+
+ themeData, err := os.ReadFile(filepath.Join(p.cfg.DataDir, "themes", p.cfg.ThemeID+".yaml"))
+ if err != nil {
+ return nil, shared.FileNotFoundError
+ }
+ theme, err := parseToTheme(themeData)
+ if err != nil {
+ return nil, shared.FileUnmarshallError
+ }
+
+ designResponse := &providers.DesignResponse{
+ Layout: layout.Layout,
+ Theme: theme.Theme,
+ }
+ return designResponse, nil
+}
+
+// parseToLayout converts YAML data into a Layout object.
+func parseToLayout(data []byte) (*Layout, error) {
+ var layoutRequest layoutRequestWithID
+
+ err := yaml.Unmarshal(data, &layoutRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ // Convert layout to JSON bytes
+ var layoutJSON json.RawMessage
+ if layoutRequest.Layout != nil {
+ // Handle both map structure and string format
+ switch v := layoutRequest.Layout.(type) {
+ case string:
+ // JSON string format
+ layoutJSON = []byte(v)
+ default:
+ // Map structure - marshal to JSON
+ layoutBytes, err := json.Marshal(layoutRequest.Layout)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal layout to JSON: %w", err)
+ }
+ layoutJSON = layoutBytes
+ }
+ }
+
+ layout := &Layout{
+ ID: layoutRequest.ID,
+ DisplayName: layoutRequest.DisplayName,
+ Description: layoutRequest.Description,
+ Layout: layoutJSON,
+ }
+
+ return layout, nil
+}
+
+// parseToTheme converts YAML data into a Theme object.
+func parseToTheme(data []byte) (*Theme, error) {
+ var themeRequest themeRequestWithID
+
+ err := yaml.Unmarshal(data, &themeRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ // Convert theme to JSON bytes
+ var themeJSON json.RawMessage
+ if themeRequest.Theme != nil {
+ // Handle both map structure and string format
+ switch v := themeRequest.Theme.(type) {
+ case string:
+ // JSON string format
+ themeJSON = []byte(v)
+ default:
+ // Map structure - marshal to JSON
+ themeBytes, err := json.Marshal(themeRequest.Theme)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal theme to JSON: %w", err)
+ }
+ themeJSON = themeBytes
+ }
+ }
+
+ theme := &Theme{
+ ID: themeRequest.ID,
+ DisplayName: themeRequest.DisplayName,
+ Description: themeRequest.Description,
+ Theme: themeJSON,
+ }
+
+ return theme, nil
+}
diff --git a/esignet-service/internal/engine/executors.go b/esignet-service/internal/engine/executors.go
index 7b899434a..de88f584d 100644
--- a/esignet-service/internal/engine/executors.go
+++ b/esignet-service/internal/engine/executors.go
@@ -1,33 +1,181 @@
package engine
import (
- "github.com/thunder-id/thunderid/pkg/thunderidengine"
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
+ "crypto/rand"
+ "fmt"
+ "strings"
- "github.com/mosip/esignet/internal/engine/mock"
- "github.com/mosip/esignet/internal/engine/mosip"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/engine/shared"
+ applog "github.com/mosip/esignet/internal/log"
+)
+
+const (
+ // ExecutorNameMosipOTP sends OTP via the MOSIP IDA API.
+ ExecutorNameMosipOTP = "MosipOtpExecutor"
+ usernameAttr = "username"
)
-// CustomExecutors returns embedder-supplied flow executors keyed by executor name.
-func CustomExecutors(authn host.AuthnProvider, provider string) map[string]thunderidengine.ExecutorInterface {
- if provider == "mosip" {
- otpAuthn, ok := authn.(mosip.OTPAuthnProvider)
- if !ok {
- return nil
+var individualIDInput = providers.Input{
+ Identifier: usernameAttr,
+ Type: providers.InputTypeText,
+ Required: true,
+}
+
+type mosipOtpExecutor struct {
+ authn shared.ConsolidatedAuthnProvider
+}
+
+var _ providers.Executor = (*mosipOtpExecutor)(nil)
+
+// NewMosipOtpExecutor creates an executor that sends OTP via the MOSIP IDA API.
+func NewMosipOtpExecutor(authn shared.ConsolidatedAuthnProvider) providers.Executor {
+ return &mosipOtpExecutor{authn: authn}
+}
+
+func (e *mosipOtpExecutor) GetName() string {
+ return ExecutorNameMosipOTP
+}
+
+func (e *mosipOtpExecutor) GetType() providers.ExecutorType {
+ return providers.ExecutorTypeAuthentication
+}
+
+func (e *mosipOtpExecutor) GetDefaultInputs() []providers.Input {
+ return nil
+}
+
+func (e *mosipOtpExecutor) GetPrerequisites() []providers.Input {
+ return []providers.Input{individualIDInput}
+}
+
+func (e *mosipOtpExecutor) GetRequiredInputs(ctx *providers.NodeContext) []providers.Input {
+ if len(ctx.NodeInputs) > 0 {
+ return ctx.NodeInputs
+ }
+ return e.GetDefaultInputs()
+}
+
+func (e *mosipOtpExecutor) GetExecutionPolicy(_ string) *providers.ExecutionPolicy {
+ return nil
+}
+
+func (e *mosipOtpExecutor) HasRequiredInputs(_ *providers.NodeContext, _ *providers.ExecutorResponse) bool {
+ return true
+}
+
+func (e *mosipOtpExecutor) GetUserIDFromContext(_ *providers.NodeContext, _ *providers.ExecutorResponse,
+ _ providers.AuthnProviderManager) string {
+ return ""
+}
+
+func (e *mosipOtpExecutor) ValidatePrerequisites(ctx *providers.NodeContext, _ *providers.ExecutorResponse,
+ _ providers.AuthnProviderManager) bool {
+ _, err := usernameFromContext(ctx)
+ return err == nil
+}
+
+func (e *mosipOtpExecutor) Execute(ctx *providers.NodeContext) (*providers.ExecutorResponse, error) {
+ execResp := &providers.ExecutorResponse{ForwardedData: make(map[string]any)}
+
+ if !e.ensurePrerequisites(ctx, execResp) {
+ return execResp, nil
+ }
+
+ username, err := usernameFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ result, serviceError := e.authn.SendOTP(ctx.Context, map[string]any{"username": username}, buildAuthnMetadata(ctx))
+ if serviceError != nil {
+ return execResp, fmt.Errorf("failed to send OTP via MOSIP API: %s", serviceError.Code)
+ }
+
+ execResp.ForwardedData["maskedEmail"] = result.MaskedEmail
+ execResp.ForwardedData["maskedMobile"] = result.MaskedMobile
+ execResp.Status = providers.ExecComplete
+ return execResp, nil
+}
+
+func (e *mosipOtpExecutor) ensurePrerequisites(
+ ctx *providers.NodeContext, execResp *providers.ExecutorResponse,
+) bool {
+ if e.ValidatePrerequisites(ctx, execResp, nil) {
+ return true
+ }
+
+ execResp.Status = providers.ExecUserInputRequired
+ execResp.Inputs = []providers.Input{individualIDInput}
+ return false
+}
+
+func buildAuthnMetadata(ctx *providers.NodeContext) *providers.AuthnMetadata {
+ appMetadata := make(map[string]any)
+
+ if ctx.Application.Metadata != nil {
+ for key, value := range ctx.Application.Metadata {
+ appMetadata[key] = value
}
- return map[string]thunderidengine.ExecutorInterface{
- mosip.ExecutorNameMosipOTP: mosip.NewMosipOtpExecutor(otpAuthn),
+ }
+
+ var clientIDs []string
+ for _, inboundConfig := range ctx.Application.InboundAuthConfig {
+ if inboundConfig.OAuthConfig != nil && inboundConfig.OAuthConfig.ClientID != "" {
+ clientIDs = append(clientIDs, inboundConfig.OAuthConfig.ClientID)
}
}
+ if len(clientIDs) > 0 {
+ appMetadata["client_ids"] = clientIDs
+ }
- if provider == "mock" {
- mockOTPAuthn, ok := authn.(mock.OTPAuthnProvider)
- if !ok {
- return nil
+ if ctx.RuntimeData == nil {
+ ctx.RuntimeData = make(map[string]string)
+ }
+
+ if _, exists := ctx.RuntimeData["ext_TransactionID"]; !exists {
+ mosipTransactionID, err := generateTransactionID()
+ if err != nil {
+ applog.GetLogger().Warn("failed to generate transaction ID", applog.Error(err))
+ } else {
+ ctx.RuntimeData["ext_TransactionID"] = mosipTransactionID
}
- return map[string]thunderidengine.ExecutorInterface{
- mosip.ExecutorNameMosipOTP: mosip.NewMosipOtpExecutor(mockOTPAuthn),
+ }
+
+ runtimeMetadata := map[string]string{
+ "authorization_request_id": ctx.RuntimeData["authorizationRequestId"],
+ "current_client_id": ctx.RuntimeData["clientId"],
+ }
+
+ for key, value := range ctx.RuntimeData {
+ // Only the ext_* runtime data keys are passed to the authn provider.
+ if strings.HasPrefix(key, "ext_") {
+ runtimeMetadata[key] = value
}
}
- return nil
+
+ return &providers.AuthnMetadata{AppMetadata: appMetadata, RuntimeMetadata: runtimeMetadata}
+}
+
+func usernameFromContext(ctx *providers.NodeContext) (string, error) {
+ if username := ctx.UserInputs[usernameAttr]; username != "" {
+ return username, nil
+ }
+ if username := ctx.RuntimeData[usernameAttr]; username != "" {
+ return username, nil
+ }
+ return "", fmt.Errorf("username not found in context")
+}
+
+func generateTransactionID() (string, error) {
+ const digitCount = 10
+ b := make([]byte, digitCount)
+ if _, err := rand.Read(b); err != nil {
+ return "", err
+ }
+ for i := range b {
+ b[i] = '0' + b[i]%10
+ }
+ return string(b), nil
}
diff --git a/esignet-service/internal/engine/flow_provider.go b/esignet-service/internal/engine/flow_provider.go
new file mode 100644
index 000000000..2efbb3bf1
--- /dev/null
+++ b/esignet-service/internal/engine/flow_provider.go
@@ -0,0 +1,46 @@
+package engine
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+
+ "github.com/stretchr/testify/assert/yaml"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/config"
+ "github.com/mosip/esignet/internal/engine/shared"
+)
+
+type flowProvider struct {
+ cfg *config.AppConfig
+}
+
+// NewFlowProvider returns a file-based flow provider backed by the configured data directory.
+func NewFlowProvider(cfg *config.AppConfig) providers.FlowProvider {
+ return &flowProvider{cfg: cfg}
+}
+
+func (p *flowProvider) GetFlowByHandle(_ context.Context, _ string, _ providers.FlowType) (
+ *providers.CompleteFlowDefinition, *common.ServiceError) {
+ return p.parseFlowDefinition()
+}
+
+func (p *flowProvider) GetFlow(_ context.Context, _ string) (*providers.CompleteFlowDefinition, *common.ServiceError) {
+ return p.parseFlowDefinition()
+}
+
+func (p *flowProvider) parseFlowDefinition() (*providers.CompleteFlowDefinition, *common.ServiceError) {
+ // Read the flow definition from file in the data directory.
+ data, err := os.ReadFile(filepath.Join(p.cfg.DataDir, "flows", p.cfg.AuthFlowID+".yaml"))
+ if err != nil {
+ return nil, shared.FileNotFoundError
+ }
+ var flowDef providers.CompleteFlowDefinition
+ err = yaml.Unmarshal(data, &flowDef)
+ if err != nil {
+ return nil, shared.FileUnmarshallError
+ }
+ return &flowDef, nil
+}
diff --git a/esignet-service/internal/engine/i18n_provider.go b/esignet-service/internal/engine/i18n_provider.go
new file mode 100644
index 000000000..8311ead78
--- /dev/null
+++ b/esignet-service/internal/engine/i18n_provider.go
@@ -0,0 +1,44 @@
+package engine
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+
+ "github.com/stretchr/testify/assert/yaml"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/config"
+ "github.com/mosip/esignet/internal/engine/shared"
+)
+
+type i18nProvider struct {
+ cfg *config.AppConfig
+}
+
+// NewI18nProvider returns a file-based i18n provider backed by the configured data directory.
+func NewI18nProvider(cfg *config.AppConfig) providers.I18nProvider {
+ return &i18nProvider{cfg: cfg}
+}
+
+func (p *i18nProvider) ResolveTranslations(
+ _ context.Context,
+ language string,
+ _ string,
+) (*providers.LanguageTranslationsResponse, *common.ServiceError) {
+ data, err := os.ReadFile(filepath.Join(p.cfg.DataDir, "i18n", language+".yaml"))
+ if err != nil {
+ return nil, shared.FileNotFoundError
+ }
+ var translations providers.LanguageTranslationsResponse
+ err = yaml.Unmarshal(data, &translations)
+ if err != nil {
+ return nil, shared.FileUnmarshallError
+ }
+ return &translations, nil
+}
+
+func (p *i18nProvider) ListLanguages(_ context.Context) ([]string, *common.ServiceError) {
+ return []string{"en"}, nil
+}
diff --git a/esignet-service/internal/engine/mock/mock_authn.go b/esignet-service/internal/engine/mock/mock_authn.go
index 0ca456ef5..cbde94660 100644
--- a/esignet-service/internal/engine/mock/mock_authn.go
+++ b/esignet-service/internal/engine/mock/mock_authn.go
@@ -2,54 +2,61 @@ package mock
import (
"context"
- "encoding/json"
- "errors"
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
- "github.com/mosip/esignet/internal/engine/mosip"
+ "github.com/mosip/esignet/internal/engine/shared"
)
-// OTPAuthnProvider extends host.AuthnProvider with OTP send capability.
-type OTPAuthnProvider interface {
- host.AuthnProvider
- SendOTP(_ context.Context, identifiers map[string]any, metadata *host.AuthnMetadata) (*mosip.SendOTPResult, error)
-}
-
type mockAuthnProvider struct {
}
// NewMockAuthnProvider creates a new mock authentication provider.
-func NewMockAuthnProvider() host.AuthnProvider {
+func NewMockAuthnProvider() shared.ConsolidatedAuthnProvider {
return &mockAuthnProvider{}
}
-var _ OTPAuthnProvider = (*mockAuthnProvider)(nil)
-
-func (p *mockAuthnProvider) SendOTP(_ context.Context, _ map[string]any,
- _ *host.AuthnMetadata) (*mosip.SendOTPResult, error) {
- return &mosip.SendOTPResult{
+func (p *mockAuthnProvider) SendOTP(_ context.Context, _ map[string]interface{},
+ _ *providers.AuthnMetadata) (*shared.SendOTPResult, *common.ServiceError) {
+ return &shared.SendOTPResult{
MaskedEmail: "maskedEmail",
MaskedMobile: "maskedMobile",
}, nil
}
-func (p *mockAuthnProvider) Authenticate(_ context.Context, identifiers, _ map[string]any, _ *host.AuthnMetadata) (*host.AuthnResult, error) {
- username, ok := identifiers["username"].(string)
- if !ok || username == "" {
- return nil, errors.New("missing or invalid username in identifiers")
- }
- return &host.AuthnResult{
- Authenticated: true,
- UserID: username,
- AuthToken: "authToken",
- Attributes: json.RawMessage(`{"email": "mock@email.com", "mobile": "999999999"}`),
- }, nil
+func (p *mockAuthnProvider) AuthenticateUser(_ context.Context, _, _ map[string]interface{},
+ _ *providers.RequestedAttributes,
+ _ *providers.AuthnMetadata,
+ authUser providers.AuthUser) (providers.AuthUser, providers.AuthenticatedClaims, *common.ServiceError) {
+ authUser.SetAttributeToken("authToken")
+ authUser.SetEntityReferenceToken("entityReferenceToken")
+ return authUser, nil, nil
+}
+
+func (p *mockAuthnProvider) GetEntityReference(_ context.Context, authUser providers.AuthUser) (
+ providers.AuthUser, *providers.EntityReference, *common.ServiceError) {
+ return authUser, nil, nil
+}
+
+func (p *mockAuthnProvider) GetUserAvailableAttributes(_ context.Context,
+ _ providers.AuthUser) (*providers.AttributesResponse, *common.ServiceError) {
+ return nil, nil
}
-func (p *mockAuthnProvider) GetAttributes(_ context.Context, _ string, _ *host.RequestedAttributes,
- _ *host.GetAttributesMetadata) (*host.GetAttributesResult, error) {
- return &host.GetAttributesResult{
- Attributes: json.RawMessage(`{"email": "mock@email.com", "mobile": "999999999"}`),
+func (p *mockAuthnProvider) GetUserAttributes(_ context.Context,
+ _ *providers.RequestedAttributes,
+ _ *providers.GetAttributesMetadata,
+ authUser providers.AuthUser) (providers.AuthUser, *providers.AttributesResponse, *common.ServiceError) {
+
+ return authUser, &providers.AttributesResponse{
+ Attributes: map[string]*providers.AttributeResponse{
+ "email": {
+ Value: "mock@email.com",
+ },
+ "mobile": {
+ Value: "999999999",
+ },
+ },
}, nil
}
diff --git a/esignet-service/internal/engine/mosip/config.go b/esignet-service/internal/engine/mosip/config.go
index 443a878b5..1f56cef06 100644
--- a/esignet-service/internal/engine/mosip/config.go
+++ b/esignet-service/internal/engine/mosip/config.go
@@ -1,9 +1,11 @@
// Package mosip provides MOSIP IDA authentication and OTP executors for the embedder.
package mosip
-import "os"
+import (
+ "fmt"
-const defaultMosipEnv = "Staging"
+ "github.com/kelseyhightower/envconfig"
+)
// Config holds MOSIP IDA integration settings.
type Config struct {
@@ -18,41 +20,73 @@ type Config struct {
P12Password string
}
+// mosipSpec is the environment-variable layout for MOSIP IDA settings.
+//
+// The P12 keystore is required: the provider cannot sign any IDA request
+// without it, so a missing value fails startup (this spec is only processed
+// when AUTHN_PROVIDER=mosip).
+//
+// LicenseKey and APIInternalHost are optional: they are only used to derive the
+// endpoint URLs when those are not supplied individually. A deployment that sets
+// every *_IDA_*_URL explicitly never reads them, so they carry no required tag.
+type mosipSpec struct {
+ LicenseKey string `envconfig:"MOSIP_ESIGNET_MISP_KEY"`
+ APIInternalHost string `envconfig:"MOSIP_API_INTERNAL_HOST"`
+ IDAPartnerCertURL string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_IDA_CERT_URL"`
+ SendOTPBaseURL string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_IDA_SEND_OTP_URL"`
+ KYCAuthBaseURL string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_IDA_KYC_AUTH_URL"`
+ KYCExchangeBaseURL string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_IDA_KYC_EXCHANGE_URL"`
+ DomainURI string `envconfig:"MOSIP_ESIGNET_DOMAIN_URL"`
+ Env string `envconfig:"IDA_AUTHENTICATOR_ENV" default:"Staging"`
+ P12Path string `envconfig:"MOSIP_P12_PATH" required:"true"`
+ P12Password string `envconfig:"MOSIP_P12_PASSWORD" required:"true"`
+}
+
// LoadConfig reads MOSIP auth settings from environment variables.
-func LoadConfig() Config {
- licenseKey := envOrDefault("MOSIP_ESIGNET_MISP_KEY", "")
- apiBase := trimTrailingSlash(envOrDefault("MOSIP_API_INTERNAL_HOST", ""))
-
- return Config{
- LicenseKey: licenseKey,
- IDAPartnerCertificateURL: envOrDefault(
- "MOSIP_ESIGNET_AUTHENTICATOR_IDA_CERT_URL",
- apiBase+"/mosip-certs/ida-partner.cer",
- ),
- SendOTPBaseURL: envOrDefault(
- "MOSIP_ESIGNET_AUTHENTICATOR_IDA_SEND_OTP_URL",
- apiBase+"/idauthentication/v1/otp/"+licenseKey+"/",
- ),
- KYCAuthBaseURL: envOrDefault(
- "MOSIP_ESIGNET_AUTHENTICATOR_IDA_KYC_AUTH_URL",
- apiBase+"/idauthentication/v1/kyc-auth/delegated/"+licenseKey+"/",
- ),
- KYCExchangeBaseURL: envOrDefault(
- "MOSIP_ESIGNET_AUTHENTICATOR_IDA_KYC_EXCHANGE_URL",
- apiBase+"/idauthentication/v1/kyc-exchange/delegated/"+licenseKey+"/",
- ),
- DomainURI: envOrDefault("MOSIP_ESIGNET_DOMAIN_URL", apiBase),
- Env: envOrDefault("IDA_AUTHENTICATOR_ENV", defaultMosipEnv),
- P12Path: os.Getenv("MOSIP_P12_PATH"),
- P12Password: os.Getenv("MOSIP_P12_PASSWORD"),
+//
+// The IDA endpoint URLs and the domain URI default to values derived from
+// MOSIP_API_INTERNAL_HOST (and the license key) when not overridden explicitly.
+func LoadConfig() (*Config, error) {
+ var s mosipSpec
+ if err := envconfig.Process("", &s); err != nil {
+ return nil, fmt.Errorf("loading mosip config: %w", err)
}
-}
-func envOrDefault(key, fallback string) string {
- if value := os.Getenv(key); value != "" {
- return value
+ licenseKey := s.LicenseKey
+ apiBase := trimTrailingSlash(s.APIInternalHost)
+
+ certURL := s.IDAPartnerCertURL
+ if certURL == "" {
+ certURL = apiBase + "/mosip-certs/ida-partner.cer"
+ }
+ sendOTP := s.SendOTPBaseURL
+ if sendOTP == "" {
+ sendOTP = apiBase + "/idauthentication/v1/otp/" + licenseKey + "/"
}
- return fallback
+ kycAuth := s.KYCAuthBaseURL
+ if kycAuth == "" {
+ kycAuth = apiBase + "/idauthentication/v1/kyc-auth/delegated/" + licenseKey + "/"
+ }
+ kycExchange := s.KYCExchangeBaseURL
+ if kycExchange == "" {
+ kycExchange = apiBase + "/idauthentication/v1/kyc-exchange/delegated/" + licenseKey + "/"
+ }
+ domainURI := s.DomainURI
+ if domainURI == "" {
+ domainURI = apiBase
+ }
+
+ return &Config{
+ LicenseKey: licenseKey,
+ IDAPartnerCertificateURL: certURL,
+ SendOTPBaseURL: sendOTP,
+ KYCAuthBaseURL: kycAuth,
+ KYCExchangeBaseURL: kycExchange,
+ DomainURI: domainURI,
+ Env: s.Env,
+ P12Path: s.P12Path,
+ P12Password: s.P12Password,
+ }, nil
}
func trimTrailingSlash(value string) string {
diff --git a/esignet-service/internal/engine/mosip/model.go b/esignet-service/internal/engine/mosip/model.go
index 15a5f5882..1b45a1468 100644
--- a/esignet-service/internal/engine/mosip/model.go
+++ b/esignet-service/internal/engine/mosip/model.go
@@ -73,12 +73,6 @@ type IdaError struct {
ErrorMessage string `json:"errorMessage,omitempty"`
}
-// SendOTPResult represents the result of an generate and notify OTP attempt.
-type SendOTPResult struct {
- MaskedEmail string `json:"maskedEmail,omitempty"`
- MaskedMobile string `json:"maskedMobile,omitempty"`
-}
-
// IdaSendOtpRequest is the MOSIP IDA send-OTP request payload.
type IdaSendOtpRequest struct {
ID string `json:"id,omitempty"`
diff --git a/esignet-service/internal/engine/mosip/mosip_authn.go b/esignet-service/internal/engine/mosip/mosip_authn.go
index ae63882b3..6b63a1046 100644
--- a/esignet-service/internal/engine/mosip/mosip_authn.go
+++ b/esignet-service/internal/engine/mosip/mosip_authn.go
@@ -24,10 +24,14 @@ import (
"strings"
"time"
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
- "software.sslmate.com/src/go-pkcs12"
+ "golang.org/x/crypto/pkcs12"
+
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
"github.com/mosip/esignet/internal/clientmgmt"
+ "github.com/mosip/esignet/internal/config"
+ "github.com/mosip/esignet/internal/engine/shared"
applog "github.com/mosip/esignet/internal/log"
)
@@ -41,47 +45,46 @@ const (
mosipEnvStaging = "Staging" // default MOSIP_ENV; see config.LoadMosipAuthn
)
-// OTPAuthnProvider extends host.AuthnProvider with OTP send capability.
-type OTPAuthnProvider interface {
- host.AuthnProvider
- SendOTP(_ context.Context, identifiers map[string]any, metadata *host.AuthnMetadata) (*SendOTPResult, error)
-}
-
type mosipAuthnProvider struct {
- cfg Config
+ appConfig *config.AppConfig
client *http.Client
clientSvc *clientmgmt.Service
+ cfg Config
}
-// NewMosipAuthnProvider creates a MOSIP host.AuthnProvider with OTP send support.
-func NewMosipAuthnProvider(cfg Config, clientSvc *clientmgmt.Service) (host.AuthnProvider, error) {
+// NewMosipAuthnProvider creates a MOSIP providers.AuthnProviderManager with OTP send support.
+func NewMosipAuthnProvider(cfg *config.AppConfig, clientSvc *clientmgmt.Service) (shared.ConsolidatedAuthnProvider, error) {
+ mosipCfg, err := LoadConfig()
+ if err != nil {
+ return nil, err
+ }
provider := &mosipAuthnProvider{
- cfg: cfg,
+ appConfig: cfg,
client: newHTTPClient(),
clientSvc: clientSvc,
+ cfg: *mosipCfg,
}
return provider, nil
}
-var _ OTPAuthnProvider = (*mosipAuthnProvider)(nil)
-
-// Authenticate authenticates a user using the MOSIP IDA API.
-func (p *mosipAuthnProvider) Authenticate(_ context.Context, identifiers, credentials map[string]interface{},
- authnMetadata *host.AuthnMetadata) (*host.AuthnResult, error) {
+func (p *mosipAuthnProvider) AuthenticateUser(_ context.Context, identifiers, credentials map[string]interface{},
+ _ *providers.RequestedAttributes,
+ metadata *providers.AuthnMetadata,
+ authUser providers.AuthUser) (providers.AuthUser, providers.AuthenticatedClaims, *common.ServiceError) {
- relyingPartyID, clientID, err := p.getApplicationAndClientID(authnMetadata.RuntimeMetadata)
+ relyingPartyID, clientID, err := p.getApplicationAndClientID(metadata.RuntimeMetadata)
if err != nil {
- return nil, err
+ return authUser, nil, shared.ClientNotFoundError
}
individualID, ok := identifiers["username"].(string)
if !ok || individualID == "" {
- return nil, errors.New("missing or invalid individual_id in identifiers")
+ return authUser, nil, shared.InvalidIndividualIDError
}
- transactionID, err := GenerateTransactionID(authnMetadata.RuntimeMetadata)
+ transactionID, err := GenerateTransactionID(metadata.RuntimeMetadata)
if err != nil {
- return nil, fmt.Errorf("failed to generate transaction ID: %w", err)
+ return authUser, nil, shared.InvalidRequestError
}
claimsMetadataRequired := false
@@ -99,7 +102,7 @@ func (p *mosipAuthnProvider) Authenticate(_ context.Context, identifiers, creden
}
if len(credentials) == 0 {
- return nil, errors.New("missing or invalid credentials")
+ return authUser, nil, shared.InvalidRequestError
}
authRequest := &AuthRequest{
Timestamp: requestTime,
@@ -112,60 +115,60 @@ func (p *mosipAuthnProvider) Authenticate(_ context.Context, identifiers, creden
} else if password, ok := credentials["password"].(string); ok && password != "" {
authRequest.Password = password
credentialSet = true
- } else if encodedBiometric, ok := credentials["biometric"].(string); ok && encodedBiometric != "" {
+ } else if encodedBiometric, ok := biometricCredential(credentials); ok {
decodedBiometric, err := B64Decode(encodedBiometric)
if err != nil {
- return nil, fmt.Errorf("failed to decode biometric data: %w", err)
+ return authUser, nil, shared.InvalidRequestError
}
var biometrics []Biometric
if err := json.Unmarshal(decodedBiometric, &biometrics); err != nil {
- return nil, fmt.Errorf("failed to unmarshal biometric data: %w", err)
+ return authUser, nil, shared.InvalidRequestError
}
if len(biometrics) == 0 {
- return nil, errors.New("missing or invalid biometric data")
+ return authUser, nil, shared.InvalidRequestError
}
authRequest.Biometrics = biometrics
credentialSet = true
}
if !credentialSet {
- return nil, errors.New("missing or invalid credentials")
+ return authUser, nil, shared.InvalidRequestError
}
authRequestBytes, err := json.Marshal(authRequest)
if err != nil {
- return nil, fmt.Errorf("failed to marshal auth request: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
requestHash, err := GenerateHashWithErr(authRequestBytes)
if err != nil {
- return nil, fmt.Errorf("failed to hash auth request: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
hexEncodedRequestHash, err := EncodeBytesToHexUpper(requestHash)
if err != nil {
- return nil, fmt.Errorf("failed to encode request hash: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
symmetricKey, err := GenerateAESKey()
if err != nil {
- return nil, fmt.Errorf("failed to generate symmetric key: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
encryptedRequest, err := SymmetricEncrypt(authRequestBytes, symmetricKey)
if err != nil {
- return nil, fmt.Errorf("failed to encrypt request: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
encryptedRequestHash, err := SymmetricEncrypt(hexEncodedRequestHash, symmetricKey)
if err != nil {
- return nil, fmt.Errorf("failed to encrypt request hash: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
generatedCert, err := p.fetchIDAPartnerCertificate()
if err != nil {
- return nil, fmt.Errorf("failed to fetch IDA partner certificate: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
encryptedSessionKey, err := AsymmetricEncrypt(generatedCert.PublicKey.(*rsa.PublicKey), symmetricKey)
if err != nil {
- return nil, fmt.Errorf("failed to encrypt session key: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
certThumbprint, err := GetCertificateThumbprint(generatedCert)
if err != nil {
- return nil, fmt.Errorf("failed to get certificate thumbprint: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
idaKycAuthRequest.RequestSessionKey = B64EncodeBytes(encryptedSessionKey)
@@ -175,47 +178,62 @@ func (p *mosipAuthnProvider) Authenticate(_ context.Context, identifiers, creden
requestBytes, err := json.Marshal(idaKycAuthRequest)
if err != nil {
- return nil, fmt.Errorf("failed to marshal IDA KYC auth request: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
requestSignature, err := p.getRequestSignature(requestBytes)
if err != nil {
- return nil, fmt.Errorf("failed to get request signature: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
- authResult, authnErr := p.callKycAuthEndpoint(requestBytes, requestSignature, relyingPartyID, clientID, claimsMetadataRequired)
- if authnErr != nil {
- return nil, authnErr
+ psut, kycToken, err := p.callKycAuthEndpoint(requestBytes, requestSignature, relyingPartyID, clientID, claimsMetadataRequired)
+ if err != nil {
+ return authUser, nil, shared.AuthenticationFailedError
}
- authResult.AuthToken = strings.Join([]string{authResult.AuthToken, individualID}, "||")
- return authResult, nil
+ authUser.SetAttributeToken(strings.Join([]string{kycToken, individualID}, "||"))
+ authUser.SetEntityReferenceToken(psut)
+ return authUser, nil, nil
+}
+
+func (p *mosipAuthnProvider) GetEntityReference(_ context.Context, authUser providers.AuthUser) (
+ providers.AuthUser, *providers.EntityReference, *common.ServiceError) {
+
+ return authUser, nil, nil
}
-func (p *mosipAuthnProvider) GetAttributes(_ context.Context, token string, requested *host.RequestedAttributes,
- getAttributesMetadata *host.GetAttributesMetadata) (*host.GetAttributesResult, error) {
+func (p *mosipAuthnProvider) GetUserAvailableAttributes(_ context.Context,
+ _ providers.AuthUser) (*providers.AttributesResponse, *common.ServiceError) {
+
+ return nil, nil
+}
- kycToken := strings.Split(token, "||")[0] // Extract KYC token from token (assuming format "kycToken||username||transactionID")
- username := strings.Split(token, "||")[1] // Extract username from token (assuming format "kycToken||username||transactionID")
+func (p *mosipAuthnProvider) GetUserAttributes(_ context.Context,
+ _ *providers.RequestedAttributes,
+ metadata *providers.GetAttributesMetadata,
+ authUser providers.AuthUser) (providers.AuthUser, *providers.AttributesResponse, *common.ServiceError) {
- transactionID, err := GenerateTransactionID(getAttributesMetadata.RuntimeMetadata)
+ relyingPartyID, clientID, err := p.getApplicationAndClientID(metadata.RuntimeMetadata)
if err != nil {
- return nil, fmt.Errorf("failed to generate transaction ID: %w", err)
+ return authUser, nil, shared.ClientNotFoundError
}
- consentedAttributes := []string{"sub", "name"}
-
- if requested != nil && len(requested.Names) > 0 {
- consentedAttributes = append(consentedAttributes, requested.Names...)
+ attributeToken := authUser.AttributeToken()
+ if attributeToken == nil || attributeToken == "" {
+ return authUser, nil, nil
}
- requestedNames := []string{}
- if requested != nil {
- requestedNames = requested.Names
+ kycToken := strings.Split(attributeToken.(string), "||")[0] // Extract KYC token from token (assuming format "kycToken||username")
+ username := strings.Split(attributeToken.(string), "||")[1] // Extract username from token (assuming format "kycToken||username")
+
+ transactionID, err := GenerateTransactionID(metadata.RuntimeMetadata)
+ if err != nil {
+ return authUser, nil, shared.InvalidRequestError
}
- applog.GetLogger().Debug("GetAttributes called",
- applog.Any("requestedAttributes", requestedNames),
- applog.Any("consentedAttributes", consentedAttributes))
+
+ consentedAttributes := []string{"sub", "name"}
+
+ // TODO add requested attributes to the request
idaKycExchangeRequest := &IdaKycExchangeRequest{
ID: mosipKycExchangeRequestID,
@@ -231,31 +249,35 @@ func (p *mosipAuthnProvider) GetAttributes(_ context.Context, token string, requ
requestBytes, err := json.Marshal(idaKycExchangeRequest)
if err != nil {
- return nil, fmt.Errorf("failed to marshal IDA KYC exchange request: %w", err)
+ return authUser, nil, shared.InvalidRequestError
}
- relyingPartyID, clientID, err := p.getApplicationAndClientID(getAttributesMetadata.RuntimeMetadata)
+ requestSignature, err := p.getRequestSignature(requestBytes)
if err != nil {
- return nil, err
+ return authUser, nil, shared.InvalidRequestError
}
-
- requestSignature, err := p.getRequestSignature(requestBytes)
+ attributesResponse, err := p.callKycExchangeEndpoint(requestBytes, requestSignature, relyingPartyID, clientID)
if err != nil {
- return nil, fmt.Errorf("failed to get request signature: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
- return p.callKycExchangeEndpoint(requestBytes, requestSignature, relyingPartyID, clientID)
+ return authUser, attributesResponse, nil
}
func (p *mosipAuthnProvider) SendOTP(_ context.Context, identifiers map[string]interface{},
- authnMetadata *host.AuthnMetadata) (*SendOTPResult, error) {
+ metadata *providers.AuthnMetadata) (*shared.SendOTPResult, *common.ServiceError) {
+
+ relyingPartyID, clientID, err := p.getApplicationAndClientID(metadata.RuntimeMetadata)
+ if err != nil {
+ return nil, shared.ClientNotFoundError
+ }
- transactionID, err := GenerateTransactionID(authnMetadata.RuntimeMetadata)
+ transactionID, err := GenerateTransactionID(metadata.RuntimeMetadata)
if err != nil {
- return nil, fmt.Errorf("failed to generate transaction ID: %w", err)
+ return nil, shared.InvalidRequestError
}
individualID, ok := identifiers["username"].(string)
if !ok || individualID == "" {
- return nil, errors.New("missing or invalid individual_id in identifiers")
+ return nil, shared.InvalidRequestError
}
req := IdaSendOtpRequest{
ID: mosipSendOtpRequestID,
@@ -268,19 +290,18 @@ func (p *mosipAuthnProvider) SendOTP(_ context.Context, identifiers map[string]i
otpRequestBytes, err := json.Marshal(req)
if err != nil {
- return nil, fmt.Errorf("failed to marshal send OTP request: %w", err)
+ return nil, shared.InvalidRequestError
}
- relyingPartyID, clientID, err := p.getApplicationAndClientID(authnMetadata.RuntimeMetadata)
+ requestSignature, err := p.getRequestSignature(otpRequestBytes)
if err != nil {
- return nil, err
+ return nil, shared.InvalidRequestError
}
-
- requestSignature, err := p.getRequestSignature(otpRequestBytes)
+ sendOTPResult, err := p.callSendOtpEndpoint(otpRequestBytes, requestSignature, relyingPartyID, clientID)
if err != nil {
- return nil, fmt.Errorf("failed to get request signature: %w", err)
+ return nil, shared.SendOTPFailedError
}
- return p.callSendOtpEndpoint(otpRequestBytes, requestSignature, relyingPartyID, clientID)
+ return sendOTPResult, nil
}
func (p *mosipAuthnProvider) getApplicationAndClientID(runtimeMetadata map[string]string) (string, string, error) {
@@ -361,10 +382,35 @@ func B64EncodeString(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
}
-// B64Decode decodes base64url string (both padded and unpadded accepted)
+// B64Decode decodes base64 in unpadded URL-safe, standard, or padded URL-safe form.
+// Biometric payloads from the OIDC UI use standard base64 (btoa); internal MOSIP
+// payloads use base64url.
func B64Decode(s string) ([]byte, error) {
- // RawURLEncoding accepts both padded and unpadded input
- return base64.RawURLEncoding.DecodeString(s)
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return nil, errors.New("empty base64 input")
+ }
+
+ var lastErr error
+ for _, enc := range []*base64.Encoding{
+ base64.RawURLEncoding,
+ base64.StdEncoding,
+ base64.URLEncoding,
+ } {
+ decoded, err := enc.DecodeString(s)
+ if err == nil {
+ return decoded, nil
+ }
+ lastErr = err
+ }
+ return nil, lastErr
+}
+
+func biometricCredential(credentials map[string]interface{}) (string, bool) {
+ if val, ok := credentials["biometric"].(string); ok && val != "" {
+ return val, true
+ }
+ return "", false
}
// GenerateHashWithErr returns the SHA-256 hash of data.
@@ -596,7 +642,7 @@ func (p *mosipAuthnProvider) callSendOtpEndpoint(
signature string, // from helperService.getRequestSignature(requestBody)
relyingPartyID string,
clientID string,
-) (*SendOTPResult, error) {
+) (*shared.SendOTPResult, error) {
endpointURL, err := buildIDAEndpointURL(p.cfg.SendOTPBaseURL, relyingPartyID, clientID)
if err != nil {
return nil, fmt.Errorf("invalid send OTP URL: %w", err)
@@ -636,7 +682,7 @@ func (p *mosipAuthnProvider) callSendOtpEndpoint(
// Success path
if wrapper.Response != nil {
- return &SendOTPResult{
+ return &shared.SendOTPResult{
MaskedEmail: wrapper.Response.MaskedEmail,
MaskedMobile: wrapper.Response.MaskedMobile,
}, nil
@@ -667,15 +713,15 @@ func (p *mosipAuthnProvider) callKycAuthEndpoint(
relyingPartyID string,
clientID string,
_ bool,
-) (*host.AuthnResult, error) {
+) (string, string, error) {
endpointURL, err := buildIDAEndpointURL(p.cfg.KYCAuthBaseURL, relyingPartyID, clientID)
if err != nil {
- return nil, fmt.Errorf("invalid KYC auth URL: %w", err)
+ return "", "", err
}
req, err := http.NewRequest(http.MethodPost, endpointURL, bytes.NewReader(requestBody))
if err != nil {
- return nil, fmt.Errorf("failed to create KYC auth request: %w", err)
+ return "", "", err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
@@ -684,39 +730,35 @@ func (p *mosipAuthnProvider) callKycAuthEndpoint(
resp, err := p.client.Do(req)
if err != nil {
- return nil, fmt.Errorf("KYC auth request failed: %w", err)
+ return "", "", err
}
defer func() { _ = resp.Body.Close() }()
// Read body once
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
- return nil, fmt.Errorf("failed to read KYC auth response: %w", err)
+ return "", "", err
}
// Check status
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- return nil, fmt.Errorf("unexpected KYC auth status: %d - %s", resp.StatusCode, string(bodyBytes))
+ return "", "", err
}
// Parse response
var wrapper IdaResponseWrapper
if err := json.Unmarshal(bodyBytes, &wrapper); err != nil {
- return nil, fmt.Errorf("failed to parse IdaResponseWrapper: %w", err)
+ return "", "", err
}
// Success path
if wrapper.Response != nil && wrapper.Response.KycStatus && wrapper.Response.KycToken != "" {
- return &host.AuthnResult{
- Authenticated: wrapper.Response.KycStatus,
- UserID: wrapper.Response.AuthToken,
- AuthToken: wrapper.Response.KycToken,
- }, nil
+ return wrapper.Response.AuthToken, wrapper.Response.KycToken, nil
}
// Error path
if wrapper.Response == nil {
- return nil, errors.New("response object is missing in wrapper")
+ return "", "", err
}
applog.GetLogger().Error("IDA KYC auth error response",
@@ -724,12 +766,12 @@ func (p *mosipAuthnProvider) callKycAuthEndpoint(
applog.Any("errors", wrapper.Errors))
if len(wrapper.Errors) == 0 {
- return nil, errors.New("no errors in response wrapper")
+ return "", "", err
}
// Take first error (common pattern)
firstErr := wrapper.Errors[0]
- return nil, fmt.Errorf("%s: %s", firstErr.ErrorMessage, firstErr.ActionMessage)
+ return "", "", fmt.Errorf("%s: %s", firstErr.ErrorMessage, firstErr.ActionMessage)
}
// PerformKycExchange sends the KYC exchange request to IDA and processes the response
@@ -738,7 +780,7 @@ func (p *mosipAuthnProvider) callKycExchangeEndpoint(
signature string,
relyingPartyID string,
clientID string,
-) (*host.GetAttributesResult, error) {
+) (*providers.AttributesResponse, error) {
endpointURL, err := buildIDAEndpointURL(p.cfg.KYCExchangeBaseURL, relyingPartyID, clientID)
if err != nil {
return nil, fmt.Errorf("invalid KYC exchange URL: %w", err)
@@ -782,12 +824,15 @@ func (p *mosipAuthnProvider) callKycExchangeEndpoint(
if err != nil {
return nil, fmt.Errorf("failed to decode encrypted KYC JWT: %w", err)
}
- attributesJSON, err := json.Marshal(userattributes)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal user attributes: %w", err)
- }
- return &host.GetAttributesResult{
- Attributes: attributesJSON,
+ return &providers.AttributesResponse{
+ Attributes: map[string]*providers.AttributeResponse{
+ "sub": {
+ Value: userattributes["sub"],
+ },
+ "name": {
+ Value: userattributes["name"],
+ },
+ },
}, nil
}
@@ -849,13 +894,8 @@ func buildIDAEndpointURL(baseURL, relyingPartyID, clientID string) (string, erro
}
func (p *mosipAuthnProvider) getRequestSignature(requestBody []byte) (string, error) {
- if p.cfg.P12Path == "" {
- return "", errors.New("MOSIP_P12_PATH is not configured")
- }
- if p.cfg.P12Password == "" {
- return "", errors.New("MOSIP_P12_PASSWORD is not configured")
- }
-
+ // P12Path and P12Password are marked required:"true" in mosipSpec, so the
+ // config layer guarantees they are set before the provider is constructed.
encodedRequestBody := B64EncodeBytes(requestBody)
privateKey, signedCertificate, err := LoadRSAPrivateKeyAndCertFromP12(p.cfg.P12Path, p.cfg.P12Password)
diff --git a/esignet-service/internal/engine/mosip/mosip_otp_executor.go b/esignet-service/internal/engine/mosip/mosip_otp_executor.go
deleted file mode 100644
index dfb88753c..000000000
--- a/esignet-service/internal/engine/mosip/mosip_otp_executor.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package mosip
-
-import (
- "fmt"
- "strings"
-
- "github.com/thunder-id/thunderid/pkg/thunderidengine"
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
-
- applog "github.com/mosip/esignet/internal/log"
-)
-
-const (
- // ExecutorNameMosipOTP sends OTP via the MOSIP IDA API.
- ExecutorNameMosipOTP = "MosipOtpExecutor"
- usernameAttr = "username"
-)
-
-var individualIDInput = thunderidengine.ExecutorInput{
- Identifier: usernameAttr,
- Type: "TEXT_INPUT",
- Required: true,
-}
-
-type mosipOtpExecutor struct {
- thunderidengine.ExecutorInterface
- authn OTPAuthnProvider
-}
-
-// NewMosipOtpExecutor creates an executor that sends OTP via the MOSIP IDA API.
-func NewMosipOtpExecutor(authn OTPAuthnProvider) thunderidengine.ExecutorInterface {
- return &mosipOtpExecutor{
- ExecutorInterface: thunderidengine.NewBaseExecutor(
- ExecutorNameMosipOTP,
- thunderidengine.ExecutorTypeAuthentication,
- nil,
- []thunderidengine.ExecutorInput{individualIDInput},
- ),
- authn: authn,
- }
-}
-
-func (e *mosipOtpExecutor) Execute(ctx *thunderidengine.ExecutorNodeContext) (*thunderidengine.ExecutorResponse, error) {
- execResp := &thunderidengine.ExecutorResponse{ForwardedData: make(map[string]any)}
-
- if !e.ensurePrerequisites(ctx, execResp) {
- return execResp, nil
- }
-
- username, err := usernameFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- result, err := e.authn.SendOTP(ctx.Context, map[string]any{"username": username}, buildAuthnMetadata(ctx))
- if err != nil {
- return execResp, fmt.Errorf("failed to send OTP via MOSIP API: %w", err)
- }
-
- execResp.ForwardedData["maskedEmail"] = result.MaskedEmail
- execResp.ForwardedData["maskedMobile"] = result.MaskedMobile
- execResp.Status = thunderidengine.ExecComplete
- return execResp, nil
-}
-
-func (e *mosipOtpExecutor) ensurePrerequisites(
- ctx *thunderidengine.ExecutorNodeContext, execResp *thunderidengine.ExecutorResponse,
-) bool {
- if e.ValidatePrerequisites(ctx, execResp, nil) {
- return true
- }
-
- execResp.Status = thunderidengine.ExecUserInputRequired
- execResp.Inputs = []thunderidengine.ExecutorInput{individualIDInput}
- return false
-}
-
-func buildAuthnMetadata(ctx *thunderidengine.ExecutorNodeContext) *host.AuthnMetadata {
- appMetadata := make(map[string]any)
-
- if ctx.Application.Metadata != nil {
- for key, value := range ctx.Application.Metadata {
- appMetadata[key] = value
- }
- }
-
- var clientIDs []string
- for _, inboundConfig := range ctx.Application.InboundAuthConfig {
- if inboundConfig.OAuthConfig != nil && inboundConfig.OAuthConfig.ClientID != "" {
- clientIDs = append(clientIDs, inboundConfig.OAuthConfig.ClientID)
- }
- }
- if len(clientIDs) > 0 {
- appMetadata["client_ids"] = clientIDs
- }
-
- if ctx.RuntimeData == nil {
- ctx.RuntimeData = make(map[string]string)
- }
-
- if _, exists := ctx.RuntimeData["ext_TransactionID"]; !exists {
- mosipTransactionID, err := GenerateTransactionID(nil)
- if err != nil {
- applog.GetLogger().Warn("failed to generate transaction ID", applog.Error(err))
- } else {
- ctx.RuntimeData["ext_TransactionID"] = mosipTransactionID
- }
- }
-
- runtimeMetadata := map[string]string{
- "authorization_request_id": ctx.RuntimeData["authorizationRequestId"],
- "current_client_id": ctx.RuntimeData["clientId"],
- }
-
- for key, value := range ctx.RuntimeData {
- // Only the ext_* runtime data keys are passed to the authn provider.
- if strings.HasPrefix(key, "ext_") {
- runtimeMetadata[key] = value
- }
- }
-
- return &host.AuthnMetadata{AppMetadata: appMetadata, RuntimeMetadata: runtimeMetadata}
-}
-
-func usernameFromContext(ctx *thunderidengine.ExecutorNodeContext) (string, error) {
- if username := ctx.UserInputs[usernameAttr]; username != "" {
- return username, nil
- }
- if username := ctx.RuntimeData[usernameAttr]; username != "" {
- return username, nil
- }
- return "", fmt.Errorf("username not found in context")
-}
diff --git a/esignet-service/internal/engine/observability_provider.go b/esignet-service/internal/engine/observability_provider.go
new file mode 100644
index 000000000..47542b948
--- /dev/null
+++ b/esignet-service/internal/engine/observability_provider.go
@@ -0,0 +1,23 @@
+package engine
+
+import (
+ "context"
+
+ engineconfig "github.com/thunder-id/thunderid/pkg/thunderidengine/config"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+)
+
+type observabilityProvider struct {
+ enabled bool
+}
+
+// NewObservabilityProvider returns a no-op observability provider for local development.
+func NewObservabilityProvider(cfg engineconfig.ObservabilityConfig) providers.ObservabilityProvider {
+ return &observabilityProvider{enabled: cfg.Enabled}
+}
+
+func (p *observabilityProvider) PublishEvent(_ context.Context, _ *providers.Event) {}
+
+func (p *observabilityProvider) IsEnabled() bool {
+ return p.enabled
+}
diff --git a/esignet-service/internal/engine/ou_provider.go b/esignet-service/internal/engine/ou_provider.go
new file mode 100644
index 000000000..79019a180
--- /dev/null
+++ b/esignet-service/internal/engine/ou_provider.go
@@ -0,0 +1,43 @@
+package engine
+
+import (
+ "context"
+
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/config"
+)
+
+type ouProvider struct {
+ cfg *config.AppConfig
+}
+
+// NewOUProvider returns a stub organization unit provider for local development.
+func NewOUProvider(cfg *config.AppConfig) providers.OrganizationUnitProvider {
+ return &ouProvider{cfg: cfg}
+}
+
+func (p *ouProvider) GetOrganizationUnit(_ context.Context, _ string) (providers.OrganizationUnit, *common.ServiceError) {
+ return providers.OrganizationUnit{}, nil
+}
+
+func (p *ouProvider) GetOrganizationUnitList(_ context.Context, _ int, _ int, _ *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError) {
+ return &providers.OrganizationUnitListResponse{}, nil
+}
+
+func (p *ouProvider) CreateOrganizationUnit(_ context.Context, _ providers.OrganizationUnitRequestWithID) (providers.OrganizationUnit, *common.ServiceError) {
+ return providers.OrganizationUnit{}, nil
+}
+
+func (p *ouProvider) IsParent(_ context.Context, _, _ string) (bool, *common.ServiceError) {
+ return true, nil
+}
+
+func (p *ouProvider) IsOrganizationUnitExists(_ context.Context, _ string) (bool, *common.ServiceError) {
+ return true, nil
+}
+
+func (p *ouProvider) GetOrganizationUnitChildren(_ context.Context, _ string, _ int, _ int, _ *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError) {
+ return &providers.OrganizationUnitListResponse{}, nil
+}
diff --git a/esignet-service/internal/engine/resource_provider.go b/esignet-service/internal/engine/resource_provider.go
new file mode 100644
index 000000000..612dd02de
--- /dev/null
+++ b/esignet-service/internal/engine/resource_provider.go
@@ -0,0 +1,37 @@
+package engine
+
+import (
+ "context"
+
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+
+ "github.com/mosip/esignet/internal/config"
+)
+
+type resourceProvider struct {
+ cfg *config.AppConfig
+}
+
+// NewResourceProvider returns a stub resource server provider for local development.
+func NewResourceProvider(cfg *config.AppConfig) providers.ResourceServerProvider {
+ return &resourceProvider{cfg: cfg}
+}
+
+func (p *resourceProvider) GetResourceServerByIdentifier(
+ _ context.Context, _ string,
+) (*providers.ResourceServer, *common.ServiceError) {
+ return &providers.ResourceServer{}, nil
+}
+
+func (p *resourceProvider) ValidatePermissions(
+ _ context.Context, _ string, _ []string,
+) ([]string, *common.ServiceError) {
+ return []string{}, nil
+}
+
+func (p *resourceProvider) FindResourceServersByPermissions(
+ _ context.Context, _ []string,
+) ([]providers.ResourceServer, *common.ServiceError) {
+ return []providers.ResourceServer{}, nil
+}
diff --git a/esignet-service/internal/engine/shared/errors.go b/esignet-service/internal/engine/shared/errors.go
new file mode 100644
index 000000000..7cdefcd4e
--- /dev/null
+++ b/esignet-service/internal/engine/shared/errors.go
@@ -0,0 +1,116 @@
+// Package shared provides common types and errors for authentication providers.
+package shared
+
+import "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+
+// NotImplementedError is returned when a feature is not implemented.
+var NotImplementedError = &common.ServiceError{
+ Code: "not_implemented",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "not_implemented",
+ DefaultValue: "Not implemented",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "not_implemented_description",
+ DefaultValue: "The feature is not implemented",
+ },
+}
+
+// ClientNotFoundError is returned when a client is not found.
+var ClientNotFoundError = &common.ServiceError{
+ Code: "client_not_found",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "client_not_found",
+ DefaultValue: "Client not found",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "client_not_found_description",
+ DefaultValue: "The client with the given ID was not found",
+ },
+}
+
+// InvalidIndividualIDError is returned when the individual_id in identifiers is missing or invalid.
+var InvalidIndividualIDError = &common.ServiceError{
+ Code: "missing_or_invalid_individual_id",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "missing_or_invalid_individual_id",
+ DefaultValue: "Missing or invalid individual_id in identifiers",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "missing_or_invalid_individual_id_description",
+ DefaultValue: "The individual_id in identifiers is missing or invalid",
+ },
+}
+
+// InvalidRequestError is returned when the request is invalid.
+var InvalidRequestError = &common.ServiceError{
+ Code: "invalid_request",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "invalid_request",
+ DefaultValue: "Invalid request",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "invalid_request_description",
+ DefaultValue: "The request is invalid",
+ },
+}
+
+// AuthenticationFailedError is returned when the authentication failed.
+var AuthenticationFailedError = &common.ServiceError{
+ Code: "authentication_failed",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "authentication_failed",
+ DefaultValue: "Authentication failed",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "authentication_failed_description",
+ DefaultValue: "The authentication failed with the given credentials",
+ },
+}
+
+// SendOTPFailedError is returned when the send OTP failed.
+var SendOTPFailedError = &common.ServiceError{
+ Code: "send_otp_failed",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "send_otp_failed",
+ DefaultValue: "Send OTP failed",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "send_otp_failed_description",
+ DefaultValue: "The send OTP failed with the given credentials",
+ },
+}
+
+// FileNotFoundError is returned when a required configuration file is not found.
+var FileNotFoundError = &common.ServiceError{
+ Code: "file_not_found",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "file_not_found",
+ DefaultValue: "File not found",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "file_not_found_description",
+ DefaultValue: "The file with the given ID was not found",
+ },
+}
+
+// FileUnmarshallError is returned when a configuration file cannot be unmarshalled.
+var FileUnmarshallError = &common.ServiceError{
+ Code: "file_unmarshall_error",
+ Type: common.ClientErrorType,
+ Error: common.I18nMessage{
+ Key: "file_unmarshall_error",
+ DefaultValue: "File unmarshall error",
+ },
+ ErrorDescription: common.I18nMessage{
+ Key: "file_unmarshall_error_description",
+ DefaultValue: "The file could not be unmarshalled",
+ },
+}
diff --git a/esignet-service/internal/engine/shared/interface.go b/esignet-service/internal/engine/shared/interface.go
new file mode 100644
index 000000000..907a11679
--- /dev/null
+++ b/esignet-service/internal/engine/shared/interface.go
@@ -0,0 +1,15 @@
+package shared
+
+import (
+ "context"
+
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+)
+
+// ConsolidatedAuthnProvider extends providers.AuthnProviderManager with OTP send capability.
+type ConsolidatedAuthnProvider interface {
+ providers.AuthnProviderManager
+ SendOTP(_ context.Context, identifiers map[string]interface{},
+ metadata *providers.AuthnMetadata) (*SendOTPResult, *common.ServiceError)
+}
diff --git a/esignet-service/internal/engine/shared/model.go b/esignet-service/internal/engine/shared/model.go
new file mode 100644
index 000000000..c4dcb563f
--- /dev/null
+++ b/esignet-service/internal/engine/shared/model.go
@@ -0,0 +1,7 @@
+package shared
+
+// SendOTPResult represents the result of an generate and notify OTP attempt.
+type SendOTPResult struct {
+ MaskedEmail string `json:"maskedEmail,omitempty"`
+ MaskedMobile string `json:"maskedMobile,omitempty"`
+}
diff --git a/esignet-service/internal/engine/sunbird/config.go b/esignet-service/internal/engine/sunbird/config.go
index c92273687..82b23b04c 100644
--- a/esignet-service/internal/engine/sunbird/config.go
+++ b/esignet-service/internal/engine/sunbird/config.go
@@ -3,33 +3,10 @@ package sunbird
import (
"errors"
- "os"
- "strconv"
+ "fmt"
"strings"
-)
-
-// SunbirdRC env var names.
-const (
- envSunbirdIDField = "MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_AUTH_FACTOR_KBI_INDIVIDUAL_ID_FIELD"
- envSunbirdFieldDetails = "MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_AUTH_FACTOR_KBI_FIELD_DETAILS"
- envSunbirdSearchURL = "MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_AUTH_FACTOR_KBI_REGISTRY_SEARCH_URL"
- envSunbirdEntityIDField = "MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_KBI_ENTITY_ID_FIELD"
- envSunbirdClaimsMapping = "MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_IDENTITY_OPENID_CLAIMS_MAPPING"
- envSunbirdEntityURL = "MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_REGISTRY_GET_URL"
- envSunbirdTimeout = "MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_REQUEST_TIMEOUT_SECS"
-)
-const (
- defaultSunbirdIDField = "policyNumber"
- defaultSunbirdEntityIDField = "osid"
- defaultSunbirdTimeoutSecs = 10
-
- defaultSunbirdFieldDetails = `[{"id":"policyNumber","type":"text","format":""},` +
- `{"id":"fullName","type":"text","format":""},` +
- `{"id":"dob","type":"date","format":"dd/mm/yyyy"}]`
-
- defaultSunbirdClaimsMapping = `{"name":"fullName","email":"email",` +
- `"phone_number":"mobile","gender":"gender","birthdate":"dob"}`
+ "github.com/kelseyhightower/envconfig"
)
// Config holds SunbirdRC registry (KBI) integration settings.
@@ -43,34 +20,54 @@ type Config struct {
TimeoutSecs int
}
-// LoadConfig reads SunbirdRC auth settings from environment variables.
+// sunbirdSpec is the environment-variable layout for SunbirdRC settings.
+//
+// Defaults are declared in envconfig `default` tags and reflect the released
+// MOSIP Insurance registry conventions. The field-details / claims-mapping
+// defaults are JSON blobs, so the embedded double quotes are backslash-escaped
+// to survive struct-tag parsing.
+type sunbirdSpec struct {
+ SearchURL string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_AUTH_FACTOR_KBI_REGISTRY_SEARCH_URL"`
+ EntityURL string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_REGISTRY_GET_URL"`
+ IDField string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_AUTH_FACTOR_KBI_INDIVIDUAL_ID_FIELD" default:"policyNumber"`
+ EntityIDField string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_KBI_ENTITY_ID_FIELD" default:"osid"`
+ FieldDetails string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_AUTH_FACTOR_KBI_FIELD_DETAILS" default:"[{\"id\":\"policyNumber\",\"type\":\"text\",\"format\":\"\"},{\"id\":\"fullName\",\"type\":\"text\",\"format\":\"\"},{\"id\":\"dob\",\"type\":\"date\",\"format\":\"dd/mm/yyyy\"}]"`
+ ClaimsMapping string `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_IDENTITY_OPENID_CLAIMS_MAPPING" default:"{\"name\":\"fullName\",\"email\":\"email\",\"phone_number\":\"mobile\",\"gender\":\"gender\",\"birthdate\":\"dob\"}"`
+ Timeout int `envconfig:"MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_REQUEST_TIMEOUT_SECS" default:"10"`
+}
+
+// LoadConfig reads SunbirdRC auth settings from environment variables. It
+// returns nil on error so a failed load can never be mistaken for a usable
+// zero-value config.
//
// SearchURL has no default and must be supplied for the sunbird provider to
-// start. The remaining fields default to the released MOSIP Insurance registry
-// conventions.
-func LoadConfig() Config {
- timeoutSecs := defaultSunbirdTimeoutSecs
- if raw := os.Getenv(envSunbirdTimeout); raw != "" {
- if secs, err := strconv.Atoi(raw); err == nil && secs > 0 {
- timeoutSecs = secs
- }
+// start (enforced by Validate). The remaining fields fall back to their
+// envconfig `default` tags.
+func LoadConfig() (*Config, error) {
+ var s sunbirdSpec
+ if err := envconfig.Process("", &s); err != nil {
+ return nil, fmt.Errorf("loading sunbird config: %w", err)
}
- return Config{
- SearchURL: strings.TrimSpace(envOrDefault(envSunbirdSearchURL, "")),
- EntityURL: trimTrailingSlash(envOrDefault(envSunbirdEntityURL, "")),
- IDField: envOrDefault(envSunbirdIDField, defaultSunbirdIDField),
- EntityIDField: envOrDefault(envSunbirdEntityIDField, defaultSunbirdEntityIDField),
- FieldDetails: envOrDefault(envSunbirdFieldDetails, defaultSunbirdFieldDetails),
- ClaimsMapping: envOrDefault(envSunbirdClaimsMapping, defaultSunbirdClaimsMapping),
- TimeoutSecs: timeoutSecs,
+ if s.Timeout <= 0 {
+ return nil, fmt.Errorf("loading sunbird config: MOSIP_ESIGNET_AUTHENTICATOR_SUNBIRD_RC_REQUEST_TIMEOUT_SECS must be positive, got %d", s.Timeout)
}
+
+ return &Config{
+ SearchURL: strings.TrimSpace(s.SearchURL),
+ EntityURL: trimTrailingSlash(strings.TrimSpace(s.EntityURL)),
+ IDField: strings.TrimSpace(s.IDField),
+ EntityIDField: strings.TrimSpace(s.EntityIDField),
+ FieldDetails: strings.TrimSpace(s.FieldDetails),
+ ClaimsMapping: strings.TrimSpace(s.ClaimsMapping),
+ TimeoutSecs: s.Timeout,
+ }, nil
}
// Validate reports whether the SunbirdRC settings are usable by the provider.
//
-// It is called at provider construction rather than in LoadSunbirdAuthn, which
-// runs unconditionally for every provider; failing there would also break the
+// It is called at provider construction rather than in LoadConfig, which runs
+// unconditionally for every provider; failing there would also break the
// catalog/mosip providers. Gating here means a missing SearchURL only fails when
// AUTHN_PROVIDER=sunbird.
func (c Config) Validate() error {
@@ -86,13 +83,6 @@ func (c Config) Validate() error {
return nil
}
-func envOrDefault(key, fallback string) string {
- if value := os.Getenv(key); value != "" {
- return value
- }
- return fallback
-}
-
func trimTrailingSlash(value string) string {
for len(value) > 0 && value[len(value)-1] == '/' {
value = value[:len(value)-1]
diff --git a/esignet-service/internal/engine/sunbird/sunbird_authn.go b/esignet-service/internal/engine/sunbird/sunbird_authn.go
index 98d2de0ec..1b024de71 100644
--- a/esignet-service/internal/engine/sunbird/sunbird_authn.go
+++ b/esignet-service/internal/engine/sunbird/sunbird_authn.go
@@ -9,8 +9,10 @@ import (
"net/http"
"time"
- "github.com/thunder-id/thunderid/pkg/thunderidengine/host"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/common"
+ "github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
+ "github.com/mosip/esignet/internal/engine/shared"
applog "github.com/mosip/esignet/internal/log"
)
@@ -45,7 +47,11 @@ type sunbirdAuthnProvider struct {
// NewSunbirdAuthnProvider creates a SunbirdRC registry-backed host.AuthnProvider.
// It validates the config and parses the KBI field details once, returning an
// error when SearchURL is unset or no KBI field other than IDField is configured.
-func NewSunbirdAuthnProvider(cfg Config) (host.AuthnProvider, error) {
+func NewSunbirdAuthnProvider() (shared.ConsolidatedAuthnProvider, error) {
+ cfg, err := LoadConfig()
+ if err != nil {
+ return nil, err
+ }
if err := cfg.Validate(); err != nil {
return nil, err
}
@@ -70,25 +76,32 @@ func NewSunbirdAuthnProvider(cfg Config) (host.AuthnProvider, error) {
timeout = 10 * time.Second
}
return &sunbirdAuthnProvider{
- cfg: cfg,
+ cfg: *cfg,
client: &http.Client{Timeout: timeout},
kbiFieldIDs: kbiFieldIDs,
}, nil
}
-func (p *sunbirdAuthnProvider) Authenticate(ctx context.Context, identifiers, credentials map[string]interface{},
- _ *host.AuthnMetadata) (*host.AuthnResult, error) {
+func (p *sunbirdAuthnProvider) SendOTP(_ context.Context, _ map[string]interface{},
+ _ *providers.AuthnMetadata) (*shared.SendOTPResult, *common.ServiceError) {
+ return nil, shared.NotImplementedError
+}
+
+func (p *sunbirdAuthnProvider) AuthenticateUser(ctx context.Context, identifiers, credentials map[string]interface{},
+ _ *providers.RequestedAttributes,
+ _ *providers.AuthnMetadata,
+ authUser providers.AuthUser) (providers.AuthUser, providers.AuthenticatedClaims, *common.ServiceError) {
individualID, ok := identifiers[sunbirdIndividualIDKey].(string)
if !ok || individualID == "" {
- return nil, fmt.Errorf("missing or invalid %q in identifiers", sunbirdIndividualIDKey)
+ return authUser, nil, shared.InvalidIndividualIDError
}
kbiFields := make(map[string]string, len(p.kbiFieldIDs))
for _, id := range p.kbiFieldIDs {
val, ok := credentials[id].(string)
if !ok || val == "" {
- return nil, fmt.Errorf("missing or invalid KBI field %q in credentials", id)
+ return authUser, nil, shared.InvalidRequestError
}
kbiFields[id] = val
}
@@ -96,41 +109,47 @@ func (p *sunbirdAuthnProvider) Authenticate(ctx context.Context, identifiers, cr
entityID, err := p.validateKBI(ctx, individualID, kbiFields)
if err != nil {
if errors.Is(err, errSunbirdKBIAuthFailed) {
- return nil, errSunbirdKBIAuthFailed
+ return authUser, nil, shared.InvalidRequestError
}
- return nil, fmt.Errorf("sunbird registry search failed: %w", err)
+ return authUser, nil, shared.AuthenticationFailedError
}
- return &host.AuthnResult{
- Authenticated: true,
- UserID: entityID,
- AuthToken: entityID,
- }, nil
+ authUser.SetEntityReferenceToken(entityID)
+ authUser.SetAttributeToken(entityID)
+ return authUser, nil, nil
}
-func (p *sunbirdAuthnProvider) GetAttributes(ctx context.Context, token string, _ *host.RequestedAttributes,
- _ *host.GetAttributesMetadata) (*host.GetAttributesResult, error) {
-
- if p.cfg.EntityURL == "" {
- return &host.GetAttributesResult{Attributes: json.RawMessage("{}")}, nil
- }
+func (p *sunbirdAuthnProvider) GetUserAttributes(ctx context.Context,
+ _ *providers.RequestedAttributes,
+ _ *providers.GetAttributesMetadata,
+ authUser providers.AuthUser) (providers.AuthUser, *providers.AttributesResponse, *common.ServiceError) {
- entityData, err := p.fetchEntityData(ctx, token)
+ entityData, err := p.fetchEntityData(ctx, authUser.EntityReferenceToken().(string))
if err != nil {
- return nil, fmt.Errorf("failed to fetch sunbird entity data: %w", err)
+ return authUser, nil, shared.InvalidRequestError
}
mappedClaims := buildSunbirdMappedClaims(entityData, p.cfg.ClaimsMapping)
- attributesJSON, err := json.Marshal(mappedClaims)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal sunbird attributes: %w", err)
+
+ mappedClaimsMap := make(map[string]*providers.AttributeResponse, len(mappedClaims))
+ for claim, value := range mappedClaims {
+ mappedClaimsMap[claim] = &providers.AttributeResponse{Value: value}
}
- return &host.GetAttributesResult{Attributes: attributesJSON}, nil
+ return authUser, &providers.AttributesResponse{Attributes: mappedClaimsMap}, nil
+}
+
+func (p *sunbirdAuthnProvider) GetEntityReference(_ context.Context,
+ authUser providers.AuthUser) (providers.AuthUser, *providers.EntityReference, *common.ServiceError) {
+ return authUser, nil, nil
+}
+
+func (p *sunbirdAuthnProvider) GetUserAvailableAttributes(_ context.Context,
+ _ providers.AuthUser) (*providers.AttributesResponse, *common.ServiceError) {
+ return &providers.AttributesResponse{}, nil
}
-func (p *sunbirdAuthnProvider) validateKBI(ctx context.Context, individualID string,
- kbiFields map[string]string) (string, error) {
+func (p *sunbirdAuthnProvider) validateKBI(ctx context.Context, individualID string, kbiFields map[string]string) (string, error) {
filters := make(map[string]sunbirdSearchFilter, len(kbiFields)+1)
filters[p.cfg.IDField] = sunbirdSearchFilter{Eq: individualID}
diff --git a/esignet-service/make.sh b/esignet-service/make.sh
index 98469b715..700f40d53 100755
--- a/esignet-service/make.sh
+++ b/esignet-service/make.sh
@@ -57,9 +57,9 @@ fi
: "${DOCKER_IMAGE:=esignet:latest}"
: "${GOLANGCI_LINT_VERSION:=latest}"
: "${SQLC_VERSION:=v1.29.0}"
-: "${THUNDER_BRANCH:=engine}"
+: "${THUNDER_BRANCH:=main}"
: "${RACE:=1}" # set RACE=0 if no C toolchain (go test -race needs gcc on Windows)
-THUNDER_MODULE=github.com/anushasunkada/thunder/backend
+THUNDER_MODULE=github.com/thunder-id/thunderid/backend
need() {
command -v "$1" >/dev/null 2>&1 || { echo "make.sh: '$1' not found on PATH (required for this target)"; exit 1; }