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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion esignet-service/data/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ oauth:
authorization_code:
validity_period: 60 # override via MOSIP_ESIGNET_OAUTH_AUTH_CODE_LIFETIME_SECONDS
par:
expires_in: 3600 # override via MOSIP_ESIGNET_OAUTH_PAR_EXPIRY_SECONDS
expires_in: 90 # RFC 9126 §2.2 short-lived; override via MOSIP_ESIGNET_OAUTH_PAR_EXPIRY_SECONDS
dpop:
required: false
iat_window: 60
Expand Down
3 changes: 3 additions & 0 deletions esignet-service/internal/clientmgmt/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ func validateClaims(claims []string, minItems, maxItems int) error {
if maxItems > 0 && len(claims) > maxItems {
return validationErr("invalid_claim")
}
if !hasUniqueStrings(claims) {
return validationErr("invalid_claim")
}
if !containsAll(claims, allowedClaims) {
return validationErr("invalid_claim")
}
Expand Down
10 changes: 10 additions & 0 deletions esignet-service/internal/clientmgmt/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,16 @@ func (ts *ValidateTestSuite) TestValidateClaimsBounds() {
assert.NoError(t, validateClaims(nil, 0, 0))
}

func (ts *ValidateTestSuite) TestValidateClaimsDuplicates() {
t := ts.T()
assert.Equal(t, "invalid_claim", errCode(t, validateClaims([]string{"name", "name"}, 0, 0)),
"duplicate claim entries must be rejected")
assert.Equal(t, "invalid_claim", errCode(t, validateClaims([]string{"name", "email", "name"}, 0, 0)),
"duplicate claim appearing non-adjacently must be rejected")
assert.NoError(t, validateClaims([]string{"name", "email"}, 0, 0),
"distinct claims must be accepted")
}

func (ts *ValidateTestSuite) TestValidateACRsBounds() {
t := ts.T()
assert.Equal(t, "invalid_acr", errCode(t, validateACRs([]string{"mosip:idp:acr:password"}, allowedACRAll, 2, 0)))
Expand Down
4 changes: 3 additions & 1 deletion esignet-service/internal/config/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,8 @@ func applyDefaults(cfg *AppConfig) {
cfg.OAuth.AuthorizationCode.ValidityPeriod = 60
}
if cfg.OAuth.PAR.ExpiresIn <= 0 {
cfg.OAuth.PAR.ExpiresIn = 3600
// RFC 9126 §2.2 requires a short-lived request_uri; 90 s matches FAPI 2.0 guidance.
cfg.OAuth.PAR.ExpiresIn = 90
Comment thread
Md-Humair-KK marked this conversation as resolved.
Resolved
}
cfg.OAuth.AuthClass.AcrAMR = map[string][]string{
"mosip:idp:acr:generated-code": {},
Expand Down Expand Up @@ -610,6 +611,7 @@ func ApplyEnvOverrides(cfg *AppConfig) error {
}
}
if v := os.Getenv("MOSIP_ESIGNET_OAUTH_PAR_EXPIRY_SECONDS"); v != "" {
// Default: 90 s (RFC 9126 §2.2 short-lived; matches deployment.yaml and applyDefaults).
secs, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fmt.Errorf("invalid MOSIP_ESIGNET_OAUTH_PAR_EXPIRY_SECONDS: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions esignet-service/internal/config/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,12 @@ func (ts *AppConfigTestSuite) TestApplyDefaultsPARExpiresIn() {
ts.Require().EqualValues(900, cfg.OAuth.PAR.ExpiresIn)
})

t.Run("defaults to 3600 when unset", func(_ *testing.T) {
t.Run("defaults to 90 when unset", func(_ *testing.T) {
cfg := &AppConfig{}

applyDefaults(cfg)

ts.Require().EqualValues(3600, cfg.OAuth.PAR.ExpiresIn)
ts.Require().EqualValues(90, cfg.OAuth.PAR.ExpiresIn)
})
}

Expand Down
6 changes: 3 additions & 3 deletions esignet-service/internal/engine/captcha_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type captchaProvider struct {
func NewCaptchaProvider(config *config.CaptchaConfig, client *http.Client) providers.CaptchaValidationProvider {
logger := applog.GetLogger()
if !isEnabled(config) {
logger.Warn(context.Background(), "Captcha Validator URL not configured! captch token validation will be ignored.")
logger.Warn(context.Background(), "Captcha Validator URL not configured — all captcha tokens will be rejected (fail-closed). Set MOSIP_ESIGNET_CAPTCHA_VALIDATOR_URL to enable validation.")
}
return &captchaProvider{config: config, client: client, logger: logger}
}
Expand Down Expand Up @@ -66,8 +66,8 @@ func (p captchaProvider) Verify(ctx context.Context, token string) (*providers.C
}

if !isEnabled(p.config) {
p.logger.Warn(ctx, "Captcha Validator URL not configured! captch token validation will be ignored.")
return &providers.CaptchaVerificationResult{Success: true}, nil
p.logger.Warn(ctx, "Captcha Validator URL not configured — rejecting captcha token (fail-closed).")
return &providers.CaptchaVerificationResult{Success: false}, nil
}

payload, err := json.Marshal(captchaRequestWrapper{
Expand Down
4 changes: 3 additions & 1 deletion esignet-service/internal/engine/captcha_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ func (ts *CaptchaProviderTestSuite) TestCaptchaProvider_Verify_EmptyToken() {
}

func (ts *CaptchaProviderTestSuite) TestCaptchaProvider_Verify_NotConfigured() {
// When ValidatorURL is not set the provider must fail closed (Success: false)
// so that a missing configuration does not silently accept arbitrary tokens.
p := NewCaptchaProvider(&config.CaptchaConfig{}, http.DefaultClient)

result, svcErr := p.Verify(context.Background(), "some-token")
require.Nil(ts.T(), svcErr)
require.NotNil(ts.T(), result)
require.True(ts.T(), result.Success)
require.False(ts.T(), result.Success)
}

func (ts *CaptchaProviderTestSuite) TestCaptchaProvider_Verify_Success() {
Expand Down
16 changes: 14 additions & 2 deletions esignet-service/internal/engine/sunbird/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/thunder-id/thunderid/pkg/thunderidengine/common"
"github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
Expand Down Expand Up @@ -91,8 +92,19 @@ func (p *sunbirdAuthnProvider) Authenticate(ctx context.Context, identifiers, cr
kbiFields := make(map[string]string, len(p.kbiFieldIDs))
for _, id := range p.kbiFieldIDs {
val, ok := credentials[id].(string)
if !ok || val == "" {
return nil, shared.InvalidRequestError
if !ok || strings.TrimSpace(val) == "" {
return nil, &common.ServiceError{
Code: "AUTHN-MGR-1008",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Type: common.ClientErrorType,
Error: common.I18nMessage{
Key: "missing_required_field",
DefaultValue: fmt.Sprintf("Required field '%s' is missing or blank", id),
},
ErrorDescription: common.I18nMessage{
Key: "missing_required_field_description",
DefaultValue: fmt.Sprintf("The required KBI field '%s' must not be blank", id),
},
}
}
kbiFields[id] = val
}
Expand Down
21 changes: 18 additions & 3 deletions esignet-service/internal/engine/sunbird/authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ func (ts *AuthenticatorTestSuite) TestAuthenticate() {
t.Run("missing credential field", func(t *testing.T) {
p := newTestProvider("http://unused", "http://unused")
identifiers := map[string]interface{}{sunbirdIndividualIDKey: "POL123"}
credentials := map[string]interface{}{"fullName": "Jane Doe"}
credentials := map[string]interface{}{"fullName": "Jane Doe"} // "dob" absent
result, svcErr := p.Authenticate(context.Background(), identifiers, credentials, nil)
require.Nil(t, result)
require.Same(t, shared.InvalidRequestError, svcErr)
require.NotNil(t, svcErr)
require.Equal(t, "AUTHN-MGR-1008", svcErr.Code)
require.Contains(t, svcErr.Error.DefaultValue, "dob")
})

t.Run("empty credential value", func(t *testing.T) {
Expand All @@ -101,7 +103,20 @@ func (ts *AuthenticatorTestSuite) TestAuthenticate() {
credentials := map[string]interface{}{"fullName": "Jane Doe", "dob": ""}
result, svcErr := p.Authenticate(context.Background(), identifiers, credentials, nil)
require.Nil(t, result)
require.Same(t, shared.InvalidRequestError, svcErr)
require.NotNil(t, svcErr)
require.Equal(t, "AUTHN-MGR-1008", svcErr.Code)
require.Contains(t, svcErr.Error.DefaultValue, "dob")
})

t.Run("whitespace-only credential value", func(t *testing.T) {
p := newTestProvider("http://unused", "http://unused")
identifiers := map[string]interface{}{sunbirdIndividualIDKey: "POL123"}
credentials := map[string]interface{}{"fullName": "Jane Doe", "dob": " "}
result, svcErr := p.Authenticate(context.Background(), identifiers, credentials, nil)
require.Nil(t, result)
require.NotNil(t, svcErr)
require.Equal(t, "AUTHN-MGR-1008", svcErr.Code)
require.Contains(t, svcErr.Error.DefaultValue, "dob")
})

t.Run("successful match", func(t *testing.T) {
Expand Down
Loading