Compare commits

..

13 Commits

Author SHA1 Message Date
Adnan Zahir aadc19a3ca Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!335
2026-02-26 16:17:04 +07:00
Adnan Zahir aabad2b082 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!331
2026-02-20 09:53:22 +07:00
Adnan Zahir e323f42c11 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!323
2026-02-12 11:04:50 +07:00
Adnan Zahir dd2832b8fc Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!319
2026-02-07 17:00:36 +07:00
kris 04068c2a8b Merge branch 'development' into 'staging'
Create job for MR

See merge request mbugroup/lti-api!316
2026-02-06 16:51:05 +00:00
Adnan Zahir 0db1aaaab7 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!314
2026-02-06 11:31:55 +07:00
Adnan Zahir 2b258908ef Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!310
2026-02-05 10:32:25 +07:00
Adnan Zahir 74e5542726 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!299
2026-02-03 13:05:23 +07:00
Adnan Zahir ceba7c5543 Merge branch 'production' into 'staging'
Production

See merge request mbugroup/lti-api!291
2026-01-31 10:51:33 +07:00
Adnan Zahir b32789e515 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!290
2026-01-31 10:42:07 +07:00
Adnan Zahir a7611ad0b2 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!287
2026-01-30 14:43:56 +07:00
Adnan Zahir 0042cf11ce Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!285
2026-01-30 11:42:14 +07:00
Adnan Zahir b860a68db2 Merge branch 'development' into 'staging'
changes permission to redis and scope

See merge request mbugroup/lti-api!282
2026-01-29 19:12:27 +07:00
5 changed files with 35 additions and 121 deletions
-2
View File
@@ -42,8 +42,6 @@ Copy .env.example to .env and adjust the variables (e.g. DATABASE_URL, JWT secre
cp .env.example .env
```
Catatan: isi `SSO_HS_SECRET` jika ingin verifikasi token HS256 tanpa JWKS.
### 5. Setup Docker
Run initial docker.
+1 -1
View File
@@ -69,7 +69,7 @@ func setupSSO(ctx context.Context, rdb *redis.Client) {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences, config.SSOHMACSecret); err != nil {
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
lastErr = err
utils.Log.WithError(err).Warnf("SSO initialization attempt %d/%d failed", attempt, maxAttempts)
select {
-5
View File
@@ -50,7 +50,6 @@ var (
CORSMaxAge int
SSOIssuer string
SSOJWKSURL string
SSOHMACSecret string
SSOAllowedAudiences []string
SSOAuthorizeURL string
SSOTokenURL string
@@ -137,7 +136,6 @@ func init() {
// SSO integration
SSOIssuer = viper.GetString("SSO_ISSUER")
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
SSOHMACSecret = viper.GetString("SSO_HS_SECRET")
SSOAllowedAudiences = parseList("SSO_ALLOWED_AUDIENCES")
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
@@ -272,9 +270,6 @@ func ensureProdConfig() {
if SSOAuthorizeURL == "" || !strings.HasPrefix(SSOAuthorizeURL, "https://") {
panic("SSO_AUTHORIZE_URL must be https in production")
}
if strings.TrimSpace(SSOHMACSecret) == "" && strings.TrimSpace(SSOJWKSURL) == "" {
panic("SSO_JWKS_URL or SSO_HS_SECRET must be configured in production")
}
if SSOTokenURL == "" || !strings.HasPrefix(SSOTokenURL, "https://") {
panic("SSO_TOKEN_URL must be https in production")
}
+34 -71
View File
@@ -19,10 +19,9 @@ import (
)
type verifier struct {
jwks *keyfunc.JWKS
issuer string
audiences map[string]struct{}
hmacSecret []byte
jwks *keyfunc.JWKS
issuer string
audiences map[string]struct{}
}
type AccessTokenClaims struct {
@@ -59,39 +58,13 @@ var (
globalV *verifier
)
func Init(ctx context.Context, jwksURL, issuer string, audiences []string, hmacSecret string) error {
func Init(ctx context.Context, jwksURL, issuer string, audiences []string) error {
jwksURL = strings.TrimSpace(jwksURL)
issuer = strings.TrimSpace(issuer)
hmacSecret = strings.TrimSpace(hmacSecret)
if issuer == "" {
return errors.New("missing SSO issuer configuration")
if jwksURL == "" || issuer == "" {
return errors.New("missing SSO JWKS or issuer configuration")
}
audienceMap := make(map[string]struct{}, len(audiences))
for _, aud := range audiences {
aud = strings.TrimSpace(aud)
if aud == "" {
continue
}
audienceMap[aud] = struct{}{}
}
globalMu.Lock()
if hmacSecret != "" {
globalV = &verifier{
jwks: nil,
issuer: issuer,
audiences: audienceMap,
hmacSecret: []byte(hmacSecret),
}
globalMu.Unlock()
utils.Log.Infof("sso verifier initialized for issuer %s (hmac)", issuer)
return nil
}
if jwksURL == "" {
globalMu.Unlock()
return errors.New("missing SSO JWKS configuration")
}
client := &http.Client{Timeout: 5 * time.Second}
options := keyfunc.Options{
Ctx: ctx,
@@ -106,9 +79,19 @@ func Init(ctx context.Context, jwksURL, issuer string, audiences []string, hmacS
jwks, err := keyfunc.Get(jwksURL, options)
if err != nil {
globalMu.Unlock()
return fmt.Errorf("load jwks: %w", err)
}
audienceMap := make(map[string]struct{}, len(audiences))
for _, aud := range audiences {
aud = strings.TrimSpace(aud)
if aud == "" {
continue
}
audienceMap[aud] = struct{}{}
}
globalMu.Lock()
globalV = &verifier{jwks: jwks, issuer: issuer, audiences: audienceMap}
globalMu.Unlock()
@@ -130,47 +113,27 @@ func VerifyAccessToken(token string) (*VerificationResult, error) {
}
claims := &AccessTokenClaims{}
if len(v.hmacSecret) > 0 {
parser := jwt.NewParser(
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
jwt.WithIssuedAt(),
jwt.WithExpirationRequired(),
)
tok, err := parser.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("invalid token signing method")
parser := jwt.NewParser(
jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
jwt.WithIssuedAt(),
jwt.WithExpirationRequired(),
)
tok, err := parser.ParseWithClaims(token, claims, v.jwks.Keyfunc)
if err != nil {
if shouldRefreshOnVerifyError(err) {
if refreshErr := v.jwks.Refresh(context.Background(), keyfunc.RefreshOptions{IgnoreRateLimit: true}); refreshErr != nil {
utils.Log.WithError(refreshErr).Warn("sso jwks refresh after signature error failed")
} else {
tok, err = parser.ParseWithClaims(token, claims, v.jwks.Keyfunc)
}
return v.hmacSecret, nil
})
}
if err != nil {
return nil, fmt.Errorf("parse token: %w", err)
}
if !tok.Valid {
return nil, errors.New("invalid token")
}
} else {
parser := jwt.NewParser(
jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
jwt.WithIssuedAt(),
jwt.WithExpirationRequired(),
)
tok, err := parser.ParseWithClaims(token, claims, v.jwks.Keyfunc)
if err != nil {
if shouldRefreshOnVerifyError(err) {
if refreshErr := v.jwks.Refresh(context.Background(), keyfunc.RefreshOptions{IgnoreRateLimit: true}); refreshErr != nil {
utils.Log.WithError(refreshErr).Warn("sso jwks refresh after signature error failed")
} else {
tok, err = parser.ParseWithClaims(token, claims, v.jwks.Keyfunc)
}
}
if err != nil {
return nil, fmt.Errorf("parse token: %w", err)
}
}
if !tok.Valid {
return nil, errors.New("invalid token")
}
}
if !tok.Valid {
return nil, errors.New("invalid token")
}
if claims.Issuer != v.issuer {
@@ -1,42 +0,0 @@
package sso
import (
"context"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
func TestVerifyAccessTokenHMAC(t *testing.T) {
secret := "test-secret-123"
issuer := "http://localhost:8080"
aud := []string{"client:1"}
if err := Init(context.Background(), "", issuer, aud, secret); err != nil {
t.Fatalf("Init error: %v", err)
}
claims := &AccessTokenClaims{
Scope: "openid profile",
RegisteredClaims: jwt.RegisteredClaims{
Issuer: issuer,
Subject: "user:1",
Audience: jwt.ClaimStrings(aud),
IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
if err != nil {
t.Fatalf("sign token error: %v", err)
}
result, err := VerifyAccessToken(token)
if err != nil {
t.Fatalf("VerifyAccessToken error: %v", err)
}
if result.UserID != 1 {
t.Fatalf("unexpected user id: %d", result.UserID)
}
}