forked from ory/fosite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
authorize_helper.go
235 lines (213 loc) · 8.69 KB
/
authorize_helper.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
/*
* Copyright © 2015-2018 Aeneas Rekkas <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Aeneas Rekkas <[email protected]>
* @copyright 2015-2018 Aeneas Rekkas <[email protected]>
* @license Apache-2.0
*
*/
package fosite
import (
"fmt"
"html/template"
"io"
"net/url"
"regexp"
"strings"
"github.com/ory/x/errorsx"
"github.com/asaskevich/govalidator"
)
var FormPostDefaultTemplate = template.Must(template.New("form_post").Parse(`<html>
<head>
<title>Submit This Form</title>
</head>
<body onload="javascript:document.forms[0].submit()">
<form method="post" action="{{ .RedirURL }}">
{{ range $key,$value := .Parameters }}
{{ range $parameter:= $value}}
<input type="hidden" name="{{$key}}" value="{{$parameter}}"/>
{{end}}
{{ end }}
</form>
</body>
</html>`))
// MatchRedirectURIWithClientRedirectURIs if the given uri is a registered redirect uri. Does not perform
// uri validation.
//
// Considered specifications
// * https://tools.ietf.org/html/rfc6749#section-3.1.2.3
// If multiple redirection URIs have been registered, if only part of
// the redirection URI has been registered, or if no redirection URI has
// been registered, the client MUST include a redirection URI with the
// authorization request using the "redirect_uri" request parameter.
//
// When a redirection URI is included in an authorization request, the
// authorization server MUST compare and match the value received
// against at least one of the registered redirection URIs (or URI
// components) as defined in [RFC3986] Section 6, if any redirection
// URIs were registered. If the client registration included the full
// redirection URI, the authorization server MUST compare the two URIs
// using simple string comparison as defined in [RFC3986] Section 6.2.1.
//
// * https://tools.ietf.org/html/rfc6819#section-4.4.1.7
// * The authorization server may also enforce the usage and validation
// of pre-registered redirect URIs (see Section 5.2.3.5). This will
// allow for early recognition of authorization "code" disclosure to
// counterfeit clients.
// * The attacker will need to use another redirect URI for its
// authorization process rather than the target web site because it
// needs to intercept the flow. So, if the authorization server
// associates the authorization "code" with the redirect URI of a
// particular end-user authorization and validates this redirect URI
// with the redirect URI passed to the token's endpoint, such an
// attack is detected (see Section 5.2.4.5).
func MatchRedirectURIWithClientRedirectURIs(rawurl string, client Client) (*url.URL, error) {
if rawurl == "" && len(client.GetRedirectURIs()) == 1 {
if redirectURIFromClient, err := url.Parse(client.GetRedirectURIs()[0]); err == nil && IsValidRedirectURI(redirectURIFromClient) {
// If no redirect_uri was given and the client has exactly one valid redirect_uri registered, use that instead
return redirectURIFromClient, nil
}
} else if redirectTo, ok := isMatchingRedirectURI(rawurl, client.GetRedirectURIs()); rawurl != "" && ok {
// If a redirect_uri was given and the clients knows it (simple string comparison!)
// return it.
if parsed, err := url.Parse(redirectTo); err == nil && IsValidRedirectURI(parsed) {
// If no redirect_uri was given and the client has exactly one valid redirect_uri registered, use that instead
return parsed, nil
}
}
return nil, errorsx.WithStack(ErrInvalidRequest.WithHint("The 'redirect_uri' parameter does not match any of the OAuth 2.0 Client's pre-registered redirect urls."))
}
// Match a requested redirect URI against a pool of registered client URIs
//
// Test a given redirect URI against a pool of URIs provided by a registered client.
// If the OAuth 2.0 Client has loopback URIs registered either an IPv4 URI http://127.0.0.1 or
// an IPv6 URI http://[::1] a client is allowed to request a dynamic port and the server MUST accept
// it as a valid redirection uri.
//
// https://tools.ietf.org/html/rfc8252#section-7.3
// Native apps that are able to open a port on the loopback network
// interface without needing special permissions (typically, those on
// desktop operating systems) can use the loopback interface to receive
// the OAuth redirect.
//
// Loopback redirect URIs use the "http" scheme and are constructed with
// the loopback IP literal and whatever port the client is listening on.
func isMatchingRedirectURI(uri string, haystack []string) (string, bool) {
requested, err := url.Parse(uri)
if err != nil {
return "", false
}
for _, b := range haystack {
if b == uri {
return b, true
} else if isMatchingAsLoopback(requested, b) {
// We have to return the requested URL here because otherwise the port might get lost (see isMatchingAsLoopback)
// description.
return uri, true
}
}
return "", false
}
func isMatchingAsLoopback(requested *url.URL, registeredURI string) bool {
registered, err := url.Parse(registeredURI)
if err != nil {
return false
}
// Native apps that are able to open a port on the loopback network
// interface without needing special permissions (typically, those on
// desktop operating systems) can use the loopback interface to receive
// the OAuth redirect.
//
// Loopback redirect URIs use the "http" scheme and are constructed with
// the loopback IP literal and whatever port the client is listening on.
//
// Source: https://tools.ietf.org/html/rfc8252#section-7.3
if requested.Scheme == "http" &&
isLoopbackAddress(requested.Host) &&
registered.Hostname() == requested.Hostname() &&
// The port is skipped here - see codedoc above!
registered.Path == requested.Path &&
registered.RawQuery == requested.RawQuery {
return true
}
return false
}
// Check if address is either an IPv4 loopback or an IPv6 loopback-
// An optional port is ignored
func isLoopbackAddress(address string) bool {
match, _ := regexp.MatchString("^(127.0.0.1|\\[::1\\])(:?)(\\d*)$", address)
return match
}
// IsValidRedirectURI validates a redirect_uri as specified in:
//
// * https://tools.ietf.org/html/rfc6749#section-3.1.2
// * The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3.
// * The endpoint URI MUST NOT include a fragment component.
// * https://tools.ietf.org/html/rfc3986#section-4.3
// absolute-URI = scheme ":" hier-part [ "?" query ]
// * https://tools.ietf.org/html/rfc6819#section-5.1.1
func IsValidRedirectURI(redirectURI *url.URL) bool {
// We need to explicitly check for a scheme
if !govalidator.IsRequestURL(redirectURI.String()) {
return false
}
if redirectURI.Fragment != "" {
// "The endpoint URI MUST NOT include a fragment component."
return false
}
return true
}
func IsRedirectURISecure(redirectURI *url.URL) bool {
return !(redirectURI.Scheme == "http" && !IsLocalhost(redirectURI))
}
// IsRedirectURISecureStrict is stricter than IsRedirectURISecure and it does not allow custom-scheme
// URLs because they can be hijacked for native apps. Use claimed HTTPS redirects instead.
// See discussion in https://github.com/ory/fosite/pull/489.
func IsRedirectURISecureStrict(redirectURI *url.URL) bool {
return redirectURI.Scheme == "https" || (redirectURI.Scheme == "http" && IsLocalhost(redirectURI))
}
func IsLocalhost(redirectURI *url.URL) bool {
hn := redirectURI.Hostname()
return strings.HasSuffix(hn, ".localhost") || hn == "127.0.0.1" || hn == "::1" || hn == "localhost"
}
func WriteAuthorizeFormPostResponse(redirectURL string, parameters url.Values, template *template.Template, rw io.Writer) {
_ = template.Execute(rw, struct {
RedirURL string
Parameters url.Values
}{
RedirURL: redirectURL,
Parameters: parameters,
})
}
func URLSetFragment(source *url.URL, fragment url.Values) {
var f string
for k, v := range fragment {
for _, vv := range v {
if len(f) != 0 {
f += fmt.Sprintf("&%s=%s", k, vv)
} else {
f += fmt.Sprintf("%s=%s", k, vv)
}
}
}
source.Fragment = f
}
func GetPostFormHTMLTemplate(f Fosite) *template.Template {
formPostHTMLTemplate := f.FormPostHTMLTemplate
if formPostHTMLTemplate == nil {
formPostHTMLTemplate = FormPostDefaultTemplate
}
return formPostHTMLTemplate
}