1
+ // dynoidtest provides helper functions for testing code that uses DynoID
1
2
package dynoidtest
2
3
3
4
import (
4
5
"context"
5
6
"crypto/rand"
6
7
"crypto/rsa"
7
8
"encoding/json"
9
+ "fmt"
8
10
"net/http"
9
11
"net/http/httptest"
10
12
"strings"
@@ -14,43 +16,115 @@ import (
14
16
"github.com/coreos/go-oidc/v3/oidc"
15
17
"github.com/golang-jwt/jwt/v4"
16
18
jose "gopkg.in/square/go-jose.v2"
19
+
20
+ "github.com/heroku/x/dynoid"
17
21
)
18
22
19
23
const (
20
- Audience = "heroku"
24
+ // IssuerHost is the host used by the dynoidtest.Issuer
21
25
IssuerHost = "heroku.local"
26
+
27
+ DefaultSpaceID = "test" // space id used when one is not provided
28
+ DefaultAppID = "00000000-0000-0000-0000-000000000001" // app id used when one is not provided
29
+ DefaultAppName = "sushi" // app name used when one is not provided
30
+ DefaultDyno = "web.1" // dyno used when one is not provided
22
31
)
23
32
33
+ // Issuer generates test tokens and provides a client for verifying them.
24
34
type Issuer struct {
25
- key * rsa.PrivateKey
35
+ key * rsa.PrivateKey
36
+ spaceID string
37
+ tokenOpts []TokenOpt
38
+ }
39
+
40
+ // IssuerOpt allows the behavior of the issuer to be modified.
41
+ type IssuerOpt interface {
42
+ apply (* Issuer ) error
43
+ }
44
+
45
+ type issuerOptFunc func (* Issuer ) error
46
+
47
+ func (f issuerOptFunc ) apply (i * Issuer ) error {
48
+ return f (i )
26
49
}
27
50
28
- func New () (* Issuer , error ) {
29
- _ , iss , err := NewWithContext (context .Background ())
51
+ // WithSpaceID allows a spaceID to be supplied instead of using the default
52
+ func WithSpaceID (spaceID string ) IssuerOpt {
53
+ return issuerOptFunc (func (i * Issuer ) error {
54
+ i .spaceID = spaceID
55
+ return nil
56
+ })
57
+ }
58
+
59
+ // WithTokenOpts allows a default set of TokenOpt to be applied to every token
60
+ // generated by the issuer
61
+ func WithTokenOpts (opts ... TokenOpt ) IssuerOpt {
62
+ return issuerOptFunc (func (i * Issuer ) error {
63
+ i .tokenOpts = append (i .tokenOpts , opts ... )
64
+ return nil
65
+ })
66
+ }
67
+
68
+ // Create a new Issuer with the supplied opts applied
69
+ func New (opts ... IssuerOpt ) (* Issuer , error ) {
70
+ _ , iss , err := NewWithContext (context .Background (), opts ... )
30
71
return iss , err
31
72
}
32
73
33
- func NewWithContext (ctx context.Context ) (context.Context , * Issuer , error ) {
74
+ // Create a new Issuer with the supplied opts applied inheriting from the provided context
75
+ func NewWithContext (ctx context.Context , opts ... IssuerOpt ) (context.Context , * Issuer , error ) {
34
76
key , err := rsa .GenerateKey (rand .Reader , 2048 )
35
77
if err != nil {
36
78
return ctx , nil , err
37
79
}
38
80
39
- iss := & Issuer {key : key }
81
+ iss := & Issuer {key : key , spaceID : DefaultSpaceID , tokenOpts : []TokenOpt {}}
82
+ for _ , o := range opts {
83
+ if err := o .apply (iss ); err != nil {
84
+ return ctx , nil , err
85
+ }
86
+ }
87
+
40
88
ctx = oidc .ClientContext (ctx , iss .HTTPClient ())
41
89
42
90
return ctx , iss , nil
43
91
}
44
92
45
- func (iss * Issuer ) GenerateIDToken (clientID string ) (string , error ) {
93
+ // A TokenOpt modifies the way a token is minted
94
+ type TokenOpt interface {
95
+ apply (* jwt.RegisteredClaims ) error
96
+ }
97
+
98
+ type tokenOptFunc func (* jwt.RegisteredClaims ) error
99
+
100
+ func (f tokenOptFunc ) apply (i * jwt.RegisteredClaims ) error {
101
+ return f (i )
102
+ }
103
+
104
+ // WithSubject allows the Subject to be different than the default
105
+ func WithSubject (s * dynoid.Subject ) TokenOpt {
106
+ return tokenOptFunc (func (c * jwt.RegisteredClaims ) error {
107
+ c .Subject = s .String ()
108
+ return nil
109
+ })
110
+ }
111
+
112
+ // GenerateIDToken returns a new signed token as a string
113
+ func (iss * Issuer ) GenerateIDToken (clientID string , opts ... TokenOpt ) (string , error ) {
46
114
now := time .Now ()
47
115
48
116
claims := & jwt.RegisteredClaims {
49
117
Audience : jwt .ClaimStrings ([]string {clientID }),
50
118
ExpiresAt : jwt .NewNumericDate (now .Add (5 * time .Minute )),
51
119
IssuedAt : jwt .NewNumericDate (now ),
52
- Issuer : "https://oidc.heroku.local/issuers/test" ,
53
- Subject : "app:00000000-0000-0000-0000-000000000001.sushi::dyno:web.1" ,
120
+ Issuer : fmt .Sprintf ("https://oidc.heroku.local/issuers/%s" , iss .spaceID ),
121
+ Subject : (& dynoid.Subject {AppID : DefaultAppID , AppName : DefaultAppName , Dyno : DefaultDyno }).String (),
122
+ }
123
+
124
+ for _ , o := range append (iss .tokenOpts , opts ... ) {
125
+ if err := o .apply (claims ); err != nil {
126
+ return "" , err
127
+ }
54
128
}
55
129
56
130
token := jwt .NewWithClaims (jwt .SigningMethodRS256 , claims )
@@ -59,6 +133,7 @@ func (iss *Issuer) GenerateIDToken(clientID string) (string, error) {
59
133
return token .SignedString (iss .key )
60
134
}
61
135
136
+ // HTTPClient returns a client that leverages the Issuer to validate tokens.
62
137
func (iss * Issuer ) HTTPClient () * http.Client {
63
138
return & http.Client {Transport : & roundTripper {issuer : iss }}
64
139
}
@@ -72,7 +147,8 @@ type roundTripper struct {
72
147
func (rt * roundTripper ) init () {
73
148
mux := http .NewServeMux ()
74
149
75
- mux .HandleFunc ("/issuers/test/.well-known/openid-configuration" , func (w http.ResponseWriter , r * http.Request ) {
150
+ basePath := fmt .Sprintf ("/issuers/%s/.well-known" , rt .issuer .spaceID )
151
+ mux .HandleFunc (basePath + "/openid-configuration" , func (w http.ResponseWriter , r * http.Request ) {
76
152
if ! strings .EqualFold (r .Method , http .MethodGet ) {
77
153
http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
78
154
return
@@ -84,17 +160,17 @@ func (rt *roundTripper) init() {
84
160
w .WriteHeader (http .StatusOK )
85
161
86
162
_ , _ = w .Write ([]byte (`{` +
87
- `"issuer":"https://oidc.heroku.local/issuers/test ",` +
163
+ fmt . Sprintf ( `"issuer":"https://oidc.heroku.local/issuers/%s ",` , rt . issuer . spaceID ) +
88
164
`"authorization_endpoint":"/dummy/authorization",` +
89
- `"jwks_uri":"https://oidc.heroku.local/issuers/test /.well-known/jwks.json",` +
165
+ fmt . Sprintf ( `"jwks_uri":"https://oidc.heroku.local/issuers/%s /.well-known/jwks.json",` , rt . issuer . spaceID ) +
90
166
`"response_types_supported":["implicit"],` +
91
167
`"grant_types_supported":["implicit"],` +
92
168
`"subject_types_supported":["public"],` +
93
169
`"id_token_signing_alg_values_supported":["RS256"]` +
94
170
`}` ))
95
171
})
96
172
97
- mux .HandleFunc ("/issuers/test/.well-known /jwks.json" , func (w http.ResponseWriter , r * http.Request ) {
173
+ mux .HandleFunc (basePath + " /jwks.json" , func (w http.ResponseWriter , r * http.Request ) {
98
174
if ! strings .EqualFold (r .Method , http .MethodGet ) {
99
175
http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
100
176
return
0 commit comments