/
consent.go
158 lines (131 loc) · 4.86 KB
/
consent.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
package sdk
import (
"crypto/rsa"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
ejwt "github.com/ory/fosite/token/jwt"
"github.com/ory/hydra/jwk"
"github.com/ory/hydra/oauth2"
"github.com/pkg/errors"
)
// Consent is a helper for singing and verifying consent challenges. For an exemplary reference implementation, check
// https://github.com/ory/hydra-consent-app-go
type Consent struct {
KeyManager jwk.Manager
}
// ResponseRequest is being used by the consent response singing helper.
type ResponseRequest struct {
// Challenge is the original consent challenge.
Challenge string
// Subject will be the sub claim of the access token. Usually this is a resource owner (user).
Subject string
// Scopes are the scopes the resource owner granted to the application requesting the access token.
Scopes []string
// AccessTokenExtra is arbitrary data that will be available when performing token introspection or warden requests.
AccessTokenExtra interface{}
// IDTokenExtra is arbitrary data that will included as a claim in the ID Token, if requested.
IDTokenExtra interface{}
}
// ChallengeClaims are the decoded claims of a consent challenge.
type ChallengeClaims struct {
// RequestedScopes are the scopes the application requested. Each scope should be explicitly granted by
// the user.
RequestedScopes []string `json:"scp"`
// The ID of the application that initiated the OAuth2 flow.
Audience string `json:"aud"`
// RedirectURL is the url where the consent app will send the user after the consent flow has been completed.
RedirectURL string `json:"redir"`
// ExpiresAt is a unix timestamp of the expiry time.
ExpiresAt float64 `json:"exp"`
// ID is the tokens' ID which will be automatically echoed in the consent response.
ID string `json:"jti"`
}
// Valid tests if the challenge's claims are valid.
func (c *ChallengeClaims) Valid() error {
if time.Now().After(ejwt.ToTime(c.ExpiresAt)) {
return errors.Errorf("Consent challenge expired")
}
return nil
}
// VerifyChallenge verifies a consent challenge and either returns the challenge's claims if it is valid, or an
// error if it is not.
//
// claims, err := c.VerifyChallenge(challenge)
// if err != nil {
// // The challenge is invalid, or the signing key could not be retrieved
// }
// // ...
func (c *Consent) VerifyChallenge(challenge string) (*ChallengeClaims, error) {
var claims ChallengeClaims
t, err := jwt.ParseWithClaims(challenge, &claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, errors.Errorf("Unexpected signing method: %v", t.Header["alg"])
}
pk, err := c.KeyManager.GetKey(oauth2.ConsentChallengeKey, "public")
if err != nil {
return nil, err
}
rsaKey, ok := jwk.First(pk.Keys).Key.(*rsa.PublicKey)
if !ok {
return nil, errors.New("Could not convert to RSA Private Key")
}
return rsaKey, nil
})
if err != nil {
return nil, errors.Wrap(err, "The consent challenge is not a valid JSON Web Token")
}
if !t.Valid {
return nil, errors.Errorf("Consent challenge is invalid")
} else if err := claims.Valid(); err != nil {
return nil, errors.Wrap(err, "The consent challenge claims could not be verified")
}
return &claims, err
}
// DenyConsent can be used to indicate that the user denied consent. Returns a redirect url or an error
// if the challenge is invalid.
//
// redirectUrl, _ := c.DenyConsent(challenge)
// http.Redirect(w, r, redirectUrl, http.StatusFound)
func (c *Consent) DenyConsent(challenge string) (string, error) {
claims, err := c.VerifyChallenge(challenge)
if err != nil {
return "", err
}
return fmt.Sprintf("%s&consent=denied", claims.RedirectURL), nil
}
// GenerateResponse generates a consent response and returns the consent response token, or an error if it is invalid.
//
// redirectUrl, _ := c.GenerateResponse(challenge)
// http.Redirect(w, r, redirectUrl, http.StatusFound)
func (c *Consent) GenerateResponse(r *ResponseRequest) (string, error) {
challenge, err := c.VerifyChallenge(r.Challenge)
if err != nil {
return "", err
}
token := jwt.New(jwt.SigningMethodRS256)
token.Claims = jwt.MapClaims{
"jti": challenge.ID,
"scp": r.Scopes,
"aud": challenge.Audience,
"exp": challenge.ExpiresAt,
"sub": r.Subject,
"at_ext": r.AccessTokenExtra,
"id_ext": r.IDTokenExtra,
}
ks, err := c.KeyManager.GetKey(oauth2.ConsentEndpointKey, "private")
if err != nil {
return "", errors.WithStack(err)
}
rsaKey, ok := jwk.First(ks.Keys).Key.(*rsa.PrivateKey)
if !ok {
return "", errors.New("Could not convert to RSA Private Key")
}
var signature, encoded string
if encoded, err = token.SigningString(); err != nil {
return "", errors.WithStack(err)
} else if signature, err = token.Method.Sign(encoded, rsaKey); err != nil {
return "", errors.WithStack(err)
}
return fmt.Sprintf("%s&consent=%s.%s", challenge.RedirectURL, encoded, signature), nil
}