forked from ProtocolONE/authone-jwt-verifier-golang
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jwtverifier.go
354 lines (306 loc) · 10.9 KB
/
jwtverifier.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
package jwtverifier
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jws"
"github.com/paysuper/authone-jwt-verifier-golang/internal"
"github.com/paysuper/authone-jwt-verifier-golang/storage"
"github.com/paysuper/authone-jwt-verifier-golang/storage/memory"
"golang.org/x/net/context/ctxhttp"
"golang.org/x/oauth2"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
// JwtVerifier used to interact with AuthOne authorization server.
type JwtVerifier struct {
config *Config
oauth2 *oauth2.Config
storage storage.Adapter
}
// Config describes a typical 3-legged OpenId Connect flow, with both the
// client application information and the server's endpoint URLs.
type Config struct {
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret.
ClientSecret string
// RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs.
RedirectURL string
// Scope specifies optional requested permissions.
Scopes []string
// Issuer is the domain where paysuper authorization server is located.
// Without a slash at the end of the line, this is important.
Issuer string
// endpoint contains the resource server's token endpoint
// URLs. These are constants specific to each server and are
// often available via tenant-specific setting for each
// AuthOne application.
endpoint endpoint
}
// AuthUrlOption contains an additional option for authentication form URL.
type AuthUrlOption struct {
// Key defines key for oauth2 url option
Key string
// Value defines value for oauth2 url option
Value string
}
// endpoint contains the OpenID Connect 1.0 provider's authorization and token
// endpoint URLs.
type endpoint struct {
// The authorization code flow begins with the client directing the user to the /authorize endpoint.
// In this request, the client indicates the permissions it needs to acquire from the user.
// You can get the OAuth 2.0 authorization endpoint for your tenant by selecting App > Endpoints.
//
// Use the CreateAuthUrl method to generate a finished link containing predefined settings based on your configuration.
authURL string
// At this endpoint, clients receive identification data and access tokens in exchange for code
// derived from authentication.
tokenURL string
// The token introspection endpoint is generally intended for identifier-based access tokens, which represent
// a secure key to an authorisation stored with the Connect2id server.
introspectURL string
// The UserInfo endpoint is an OAuth 2.0 protected resource of the Connect2id server where client applications
// can retrieve consented claims, or assertions, about the logged in end-user.
userInfoURL string
// The Connect2id server publishes its public RSA keys as a JSON Web Key (JWK) set.
// This is done for the to enable clients and other parties to verify the authenticity of identity tokens
// issued by the server.
jwksUrl string
// revokeUrl is the URL to revoke access tokens or refresh tokens
// to notify the OpenID Connect Provider that an issued token is
// no longer needed and must be revoked. The revocation endpoint
// can revoke a token that was obtained through OpenID Connect or
// OAuth authentication.
revokeUrl string
// logoutUrl is the URL to log out user with deletion session and cookie on OAuth authentication server.
logoutUrl string
}
// NewJwtVerifier create new instance of verifier with given configuration.
func NewJwtVerifier(config Config, options ...interface{}) *JwtVerifier {
config.endpoint = endpoint{
authURL: config.Issuer + "/oauth2/auth",
tokenURL: config.Issuer + "/oauth2/token",
userInfoURL: config.Issuer + "/oauth2/userinfo",
revokeUrl: config.Issuer + "/oauth2/revoke",
introspectURL: config.Issuer + "/oauth2/introspect",
logoutUrl: config.Issuer + "/oauth2/logout",
jwksUrl: config.Issuer + "/.well-known/jwks.json",
}
conf := &oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: config.Scopes,
RedirectURL: config.RedirectURL,
Endpoint: oauth2.Endpoint{
AuthURL: config.endpoint.authURL,
TokenURL: config.endpoint.tokenURL,
},
}
j := &JwtVerifier{
config: &config,
oauth2: conf,
}
for i := range options {
if st, ok := options[i].(storage.Adapter); ok {
j.storage = st
}
}
if j.storage == nil {
j.storage = memory.NewStorage(memory.MaxSize)
}
return j
}
// SetStorage allow to set adapter for the introspection token.
// See available adapters in the storage folder.
func (j *JwtVerifier) SetStorage(a storage.Adapter) {
j.storage = a
}
// CreateAuthUrl create an URL to send the user to the initial authentication step.
func (j *JwtVerifier) CreateAuthUrl(state string, options ...AuthUrlOption) string {
var buf bytes.Buffer
buf.WriteString(j.config.endpoint.authURL)
v := url.Values{
"response_type": {"code"},
"client_id": {j.config.ClientID},
}
if j.config.RedirectURL != "" {
v.Set("redirect_uri", j.config.RedirectURL)
}
if len(j.config.Scopes) > 0 {
v.Set("scope", strings.Join(j.config.Scopes, " "))
}
if state != "" {
v.Set("state", state)
}
for _, value := range options {
v.Add(value.Key, value.Value)
}
if strings.Contains(j.config.endpoint.authURL, "?") {
buf.WriteByte('&')
} else {
buf.WriteByte('?')
}
buf.WriteString(v.Encode())
return buf.String()
}
// Exchange converts an authorization code into a token.
//
// It is used after a resource provider redirects the user back
// to the Redirect URI (the URL obtained from AuthCodeURL).
//
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
//
// The code will be in the *http.Request.FormValue("code"). Before
// calling Exchange, be sure to validate FormValue("state").
//
// Opts may include the PKCE verifier code if previously used in AuthCodeURL.
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
func (j *JwtVerifier) Exchange(ctx context.Context, code string) (*Token, error) {
t, err := j.oauth2.Exchange(ctx, code)
if err != nil {
return nil, err
}
return &Token{t}, nil
}
// Introspect check the token refresh or access is active or not. An active token is neither expired nor revoked.
// Uses token storage for temporary storage of tokens. If the token has expired or it has been revoked,
// the information will be deleted from the temporary storage.
func (j *JwtVerifier) Introspect(ctx context.Context, token string) (*IntrospectToken, error) {
introspect := &IntrospectToken{}
if i, _ := j.storage.Get(token); i != nil {
if err := json.Unmarshal(i, introspect); err != nil {
return nil, err
} else {
return introspect, nil
}
}
introspect, err := j.getIntrospect(ctx, j.config.endpoint.introspectURL, j.config.ClientID, j.config.ClientSecret, token)
if err != nil {
return nil, err
}
if false == introspect.Active {
return nil, errors.New("token isn't active")
}
if j.config.ClientID != introspect.ClientID {
return nil, errors.New("token is owned by another client")
}
if i, err := json.Marshal(introspect); err == nil {
if err := j.storage.Set(token, introspect.Exp, i); err != nil {
return nil, err
}
}
return introspect, nil
}
// GetUserInfo via UserInfo endpoint with uses AccessToken by authenticate header.
// The claims are packaged in a JSON object where the sub member denotes the subject (end-user) identifier.
func (j *JwtVerifier) GetUserInfo(ctx context.Context, token string) (*UserInfo, error) {
return j.getUserInfo(ctx, token, j.config.endpoint.userInfoURL)
}
// ValidateIdToken used to check the ID Token and returns its claims (as custom json object) in the event of its validity.
func (j *JwtVerifier) ValidateIdToken(ctx context.Context, token string) (*IdToken, error) {
// UNDONE: We must use application context
//token, err := j.
set, err := jwk.Fetch(j.config.endpoint.jwksUrl)
if err != nil {
return nil, err
}
keys := set.Keys[0]
verified, err := jws.VerifyWithJWK([]byte(token), keys)
if err != nil {
return nil, err
}
t := &IdToken{}
err = json.Unmarshal(verified, t)
if err != nil {
return nil, err
}
if t.Aud[0] != j.config.ClientID {
return nil, errors.New("token is owned by another client")
}
return t, nil
}
// Revoke used to invalidate the specified token and, if applicable, other tokens based on the same
// authorisation grant.
func (j *JwtVerifier) Revoke(ctx context.Context, token string) error {
return j.revokeToken(ctx, token, j.config.endpoint.revokeUrl)
}
// CreateLogoutUrl create an URL to send the user to the logging out step with return back to the url.
func (j *JwtVerifier) CreateLogoutUrl(url string) string {
return fmt.Sprintf("%s?redirect_uri=%s", j.config.endpoint.logoutUrl, url)
}
func (j *JwtVerifier) getIntrospect(ctx context.Context, introspectURL string, clientId string, clientSecret string, token string) (*IntrospectToken, error) {
form := url.Values{"client_id": {clientId}, "secret": {clientSecret}, "token": {token}}
req, err := http.NewRequest("POST", introspectURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
r, err := ctxhttp.Do(ctx, internal.ContextClient(ctx), req)
if err != nil {
return nil, err
}
defer r.Body.Close()
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch introspect token: %v", err)
}
if code := r.StatusCode; code < 200 || code > 299 {
return nil, &RetrieveError{
Response: r,
Body: body,
}
}
t := &IntrospectToken{}
err = json.Unmarshal(body, t)
return t, err
}
func (j *JwtVerifier) getUserInfo(ctx context.Context, t string, userInfoURL string) (*UserInfo, error) {
req, err := http.NewRequest("GET", userInfoURL, strings.NewReader(""))
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t))
r, err := ctxhttp.Do(ctx, internal.ContextClient(ctx), req)
if err != nil {
return nil, err
}
defer r.Body.Close()
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch user info: %v", err)
}
if code := r.StatusCode; code < 200 || code > 299 {
return nil, &RetrieveError{
Response: r,
Body: body,
}
}
i := &UserInfo{}
err = json.Unmarshal(body, i)
return i, err
}
func (j *JwtVerifier) revokeToken(ctx context.Context, token string, revokeUrl string) error {
form := url.Values{"token": {token}}
req, err := http.NewRequest("POST", revokeUrl, strings.NewReader(form.Encode()))
req.Header.Set("Accept", "application/json")
r, err := ctxhttp.Do(ctx, internal.ContextClient(ctx), req)
if err != nil {
return err
}
defer r.Body.Close()
if code := r.StatusCode; code < 200 || code > 299 {
return &RetrieveError{Response: r}
}
j.storage.Delete(token)
return nil
}