Skip to content

Commit 9505d1d

Browse files
committed
Add jwt_decode component for token verification
1 parent d6ca842 commit 9505d1d

2 files changed

Lines changed: 237 additions & 0 deletions

File tree

cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
_ "github.com/tiny-systems/example-module/components/json/decode"
1111
_ "github.com/tiny-systems/example-module/components/json/encode"
1212
_ "github.com/tiny-systems/example-module/components/jwt/encode"
13+
_ "github.com/tiny-systems/example-module/components/jwt/verify"
1314
_ "github.com/tiny-systems/example-module/components/xml/encode"
1415
"github.com/tiny-systems/module/cli"
1516
"os"

components/jwt/verify/verify.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package verify
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/goccy/go-json"
8+
"github.com/golang-jwt/jwt/v5"
9+
"github.com/swaggest/jsonschema-go"
10+
"github.com/tiny-systems/module/api/v1alpha1"
11+
"github.com/tiny-systems/module/module"
12+
"github.com/tiny-systems/module/registry"
13+
)
14+
15+
const (
16+
ComponentName = "jwt_decode"
17+
RequestPort = "request"
18+
ResponsePort = "response"
19+
ErrorPort = "error"
20+
)
21+
22+
type Context any
23+
24+
type Settings struct {
25+
EnableErrorPort bool `json:"enableErrorPort" required:"true" title:"Enable Error Port" description:"If error happens, error port will emit an error message"`
26+
}
27+
28+
type Error struct {
29+
Context Context `json:"context"`
30+
Error string `json:"error"`
31+
}
32+
33+
// SigningMethod carries value and possible options for verification algorithms.
34+
type SigningMethod struct {
35+
Value string
36+
Options []string
37+
}
38+
39+
func (r *SigningMethod) MarshalJSON() ([]byte, error) {
40+
return json.Marshal(r.Value)
41+
}
42+
43+
func (r *SigningMethod) UnmarshalJSON(data []byte) error {
44+
return json.Unmarshal(data, &r.Value)
45+
}
46+
47+
func (r *SigningMethod) JSONSchema() (jsonschema.Schema, error) {
48+
s := jsonschema.Schema{}
49+
s.AddType(jsonschema.String)
50+
s.WithTitle("Signing Method")
51+
s.WithDefault(r.Value)
52+
enums := make([]interface{}, len(r.Options))
53+
for k, v := range r.Options {
54+
enums[k] = v
55+
}
56+
s.WithEnum(enums...)
57+
return s, nil
58+
}
59+
60+
type Request struct {
61+
Context Context `json:"context" configurable:"true" title:"Context" description:"Arbitrary message to pass through"`
62+
SigningMethod SigningMethod `json:"signingMethod" required:"true" title:"Signing Method"`
63+
Token string `json:"token" required:"true" title:"Token" description:"JWT token to verify and decode"`
64+
Key string `json:"key" required:"true" format:"textarea" title:"Key" description:"Plain text secret or PEM formatted public key"`
65+
}
66+
67+
// Claims represents decoded JWT claims.
68+
type Claims map[string]interface{}
69+
70+
func (m Claims) JSONSchema() (jsonschema.Schema, error) {
71+
s := jsonschema.Schema{}
72+
s.AddType(jsonschema.Object)
73+
s.WithProperties(map[string]jsonschema.SchemaOrBool{
74+
"sub": (&jsonschema.Schema{}).WithTitle("Subject").WithType(jsonschema.String.Type()).ToSchemaOrBool(),
75+
"iss": (&jsonschema.Schema{}).WithTitle("Issuer").WithType(jsonschema.String.Type()).ToSchemaOrBool(),
76+
"aud": (&jsonschema.Schema{}).WithTitle("Audience").WithType(jsonschema.Array.Type()).WithItems(*(&jsonschema.Items{}).WithSchemaOrBool((&jsonschema.Schema{}).WithType(jsonschema.String.Type()).ToSchemaOrBool())).ToSchemaOrBool(),
77+
"exp": (&jsonschema.Schema{}).WithTitle("ExpiresAt").WithType(jsonschema.Integer.Type()).ToSchemaOrBool(),
78+
"nbf": (&jsonschema.Schema{}).WithTitle("NotBefore").WithType(jsonschema.Integer.Type()).ToSchemaOrBool(),
79+
"iat": (&jsonschema.Schema{}).WithTitle("IssuedAt").WithType(jsonschema.Integer.Type()).ToSchemaOrBool(),
80+
"jti": (&jsonschema.Schema{}).WithTitle("ID").WithType(jsonschema.String.Type()).ToSchemaOrBool(),
81+
})
82+
return s, nil
83+
}
84+
85+
type Response struct {
86+
Context Context `json:"context"`
87+
Claims Claims `json:"claims" configurable:"true" title:"Claims" description:"Decoded JWT claims"`
88+
}
89+
90+
type Component struct {
91+
settings Settings
92+
}
93+
94+
func (h *Component) GetInfo() module.ComponentInfo {
95+
return module.ComponentInfo{
96+
Name: ComponentName,
97+
Description: "JWT Decoder",
98+
Info: "Verifies and decodes JWT token",
99+
Tags: []string{"jwt"},
100+
}
101+
}
102+
103+
func (h *Component) Handle(ctx context.Context, handler module.Handler, port string, msg interface{}) any {
104+
switch port {
105+
case v1alpha1.SettingsPort:
106+
in, ok := msg.(Settings)
107+
if !ok {
108+
return fmt.Errorf("invalid settings")
109+
}
110+
h.settings = in
111+
112+
case RequestPort:
113+
in, ok := msg.(Request)
114+
if !ok {
115+
return fmt.Errorf("invalid input")
116+
}
117+
118+
claims, err := parseToken(in.Token, in.Key, in.SigningMethod.Value)
119+
if err != nil {
120+
if !h.settings.EnableErrorPort {
121+
return err
122+
}
123+
return handler(ctx, ErrorPort, Error{
124+
Context: in.Context,
125+
Error: err.Error(),
126+
})
127+
}
128+
129+
return handler(ctx, ResponsePort, Response{
130+
Context: in.Context,
131+
Claims: Claims(claims),
132+
})
133+
134+
default:
135+
return fmt.Errorf("port %s is not supported", port)
136+
}
137+
return nil
138+
}
139+
140+
func parseToken(tokenString, key, method string) (jwt.MapClaims, error) {
141+
keyFunc, err := keyFuncForMethod(method, key)
142+
if err != nil {
143+
return nil, err
144+
}
145+
146+
token, err := jwt.Parse(tokenString, keyFunc)
147+
if err != nil {
148+
return nil, fmt.Errorf("token verification failed: %w", err)
149+
}
150+
151+
claims, ok := token.Claims.(jwt.MapClaims)
152+
if !ok {
153+
return nil, fmt.Errorf("unexpected claims type")
154+
}
155+
156+
return claims, nil
157+
}
158+
159+
func keyFuncForMethod(method, key string) (jwt.Keyfunc, error) {
160+
switch method {
161+
case "ES256", "ES384", "ES512":
162+
pubKey, err := jwt.ParseECPublicKeyFromPEM([]byte(key))
163+
if err != nil {
164+
return nil, fmt.Errorf("parse EC public key: %w", err)
165+
}
166+
return func(*jwt.Token) (interface{}, error) { return pubKey, nil }, nil
167+
168+
case "RS256", "RS384", "RS512":
169+
pubKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(key))
170+
if err != nil {
171+
return nil, fmt.Errorf("parse RSA public key: %w", err)
172+
}
173+
return func(*jwt.Token) (interface{}, error) { return pubKey, nil }, nil
174+
175+
case "HS256", "HS384", "HS512":
176+
return func(*jwt.Token) (interface{}, error) { return []byte(key), nil }, nil
177+
178+
case "None":
179+
return func(*jwt.Token) (interface{}, error) { return jwt.UnsafeAllowNoneSignatureType, nil }, nil
180+
181+
default:
182+
return nil, fmt.Errorf("unsupported signing method: %s", method)
183+
}
184+
}
185+
186+
func (h *Component) Ports() []module.Port {
187+
ports := []module.Port{
188+
{
189+
Name: RequestPort,
190+
Label: "In",
191+
Position: module.Left,
192+
Configuration: Request{
193+
SigningMethod: SigningMethod{
194+
Value: "HS256",
195+
Options: []string{
196+
"ES256", "ES384", "ES512", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "None",
197+
},
198+
},
199+
},
200+
},
201+
{
202+
Name: ResponsePort,
203+
Position: module.Right,
204+
Label: "Out",
205+
Source: true,
206+
Configuration: Response{},
207+
},
208+
{
209+
Name: v1alpha1.SettingsPort,
210+
Label: "Settings",
211+
Configuration: h.settings,
212+
},
213+
}
214+
if !h.settings.EnableErrorPort {
215+
return ports
216+
}
217+
return append(ports, module.Port{
218+
Position: module.Bottom,
219+
Name: ErrorPort,
220+
Label: "Error",
221+
Source: true,
222+
Configuration: Error{},
223+
})
224+
}
225+
226+
func (h *Component) Instance() module.Component {
227+
return &Component{
228+
settings: Settings{},
229+
}
230+
}
231+
232+
var _ module.Component = (*Component)(nil)
233+
234+
func init() {
235+
registry.Register(&Component{})
236+
}

0 commit comments

Comments
 (0)