// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: GPL-3.0-or-later package actions import ( "fmt" "net/http" "reflect" "strings" "forgejo.org/modules/jwtx" "forgejo.org/modules/setting" "forgejo.org/modules/web" web_types "forgejo.org/modules/web/types" actions_service "forgejo.org/services/actions" "forgejo.org/services/context" ) type oidcRoutes struct { openIDConfiguration openIDConfiguration jwks map[string][]map[string]string } type openIDConfiguration struct { Issuer string `json:"issuer"` JwksURI string `json:"jwks_uri"` SubjectTypesSupported []string `json:"subject_types_supported"` ResponseTypesSupported []string `json:"response_types_supported"` ClaimsSupported []string `json:"claims_supported"` IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` ScopesSupported []string `json:"scopes_supported"` } type oidcContextKeyType struct{} var oidcContextKey = oidcContextKeyType{} // jwtSigningKey is the default signing key for JWTs. var jwtSigningKey jwtx.SigningKey // jwk is the JWK format of the jwtSigningKey. var jwk map[string]string type OIDCContext struct { *context.Base } func InitOIDC() error { var err error jwtSigningKey, err = jwtx.InitAsymmetricSigningKey(setting.Actions.IDTokenSigningPrivateKeyFile, string(setting.Actions.IDTokenSigningAlgorithm)) if err != nil { return err } jwk, err = jwtSigningKey.ToJWK() if err != nil { return fmt.Errorf("Error getting JWK from default signing key: %v", err) } jwk["use"] = "sig" return nil } func init() { web.RegisterResponseStatusProvider[*OIDCContext](func(req *http.Request) web_types.ResponseStatusProvider { return req.Context().Value(oidcContextKey).(*OIDCContext) }) } func OIDCContexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { base, baseCleanUp := context.NewBaseContext(resp, req) defer baseCleanUp() ctx := &OIDCContext{Base: base} ctx.AppendContextValue(oidcContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } } func OIDCRoutes(prefix string) *web.Route { m := web.NewRoute() prefix = strings.TrimPrefix(prefix, "/") // Standard claims claimsSupported := []string{ "sub", "aud", "exp", "iat", "iss", "nbf", } // Add custom claims by iterating over [actions_service.IDTokenCustomClaims] // and inspecting the names of the json struct tags rt := reflect.TypeFor[actions_service.IDTokenCustomClaims]() for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) v := strings.Split(f.Tag.Get("json"), ",")[0] if v == "" || v == "-" { continue } claimsSupported = append(claimsSupported, v) } o := &oidcRoutes{ openIDConfiguration: openIDConfiguration{ Issuer: setting.AppURL + prefix, JwksURI: setting.AppURL + prefix + "/.well-known/keys", SubjectTypesSupported: []string{"public"}, ResponseTypesSupported: []string{"id_token"}, ClaimsSupported: claimsSupported, IDTokenSigningAlgValuesSupported: []string{string(setting.Actions.IDTokenSigningAlgorithm)}, ScopesSupported: []string{"openid"}, }, jwks: map[string][]map[string]string{ "keys": { jwk, }, }, } m.Group("", func() { m.Get("/keys", o.keys) m.Get("/openid-configuration", o.configuration) }, OIDCContexter()) return m } func (o *oidcRoutes) configuration(ctx *OIDCContext) { ctx.JSON(http.StatusOK, o.openIDConfiguration) } func (o *oidcRoutes) keys(ctx *OIDCContext) { ctx.JSON(http.StatusOK, o.jwks) }