1 /* 2 * Copyright © 2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io> 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 * @author Aeneas Rekkas <aeneas+oss@aeneas.io> 17 * @copyright 2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io> 18 * @license Apache-2.0 19 * 20 */ 21 22 package fosite 23 24 import ( 25 "fmt" 26 "html/template" 27 "io" 28 "net/url" 29 "regexp" 30 "strings" 31 32 "github.com/ory/x/errorsx" 33 34 "github.com/asaskevich/govalidator" 35 ) 36 37 var FormPostDefaultTemplate = template.Must(template.New("form_post").Parse(`<html> 38 <head> 39 <title>Submit This Form</title> 40 </head> 41 <body onload="javascript:document.forms[0].submit()"> 42 <form method="post" action="{{ .RedirURL }}"> 43 {{ range $key,$value := .Parameters }} 44 {{ range $parameter:= $value}} 45 <input type="hidden" name="{{$key}}" value="{{$parameter}}"/> 46 {{end}} 47 {{ end }} 48 </form> 49 </body> 50 </html>`)) 51 52 // MatchRedirectURIWithClientRedirectURIs if the given uri is a registered redirect uri. Does not perform 53 // uri validation. 54 // 55 // Considered specifications 56 // * https://tools.ietf.org/html/rfc6749#section-3.1.2.3 57 // If multiple redirection URIs have been registered, if only part of 58 // the redirection URI has been registered, or if no redirection URI has 59 // been registered, the client MUST include a redirection URI with the 60 // authorization request using the "redirect_uri" request parameter. 61 // 62 // When a redirection URI is included in an authorization request, the 63 // authorization server MUST compare and match the value received 64 // against at least one of the registered redirection URIs (or URI 65 // components) as defined in [RFC3986] Section 6, if any redirection 66 // URIs were registered. If the client registration included the full 67 // redirection URI, the authorization server MUST compare the two URIs 68 // using simple string comparison as defined in [RFC3986] Section 6.2.1. 69 // 70 // * https://tools.ietf.org/html/rfc6819#section-4.4.1.7 71 // * The authorization server may also enforce the usage and validation 72 // of pre-registered redirect URIs (see Section 5.2.3.5). This will 73 // allow for early recognition of authorization "code" disclosure to 74 // counterfeit clients. 75 // * The attacker will need to use another redirect URI for its 76 // authorization process rather than the target web site because it 77 // needs to intercept the flow. So, if the authorization server 78 // associates the authorization "code" with the redirect URI of a 79 // particular end-user authorization and validates this redirect URI 80 // with the redirect URI passed to the token's endpoint, such an 81 // attack is detected (see Section 5.2.4.5). 82 func MatchRedirectURIWithClientRedirectURIs(rawurl string, client Client) (*url.URL, error) { 83 if rawurl == "" && len(client.GetRedirectURIs()) == 1 { 84 if redirectURIFromClient, err := url.Parse(client.GetRedirectURIs()[0]); err == nil && IsValidRedirectURI(redirectURIFromClient) { 85 // If no redirect_uri was given and the client has exactly one valid redirect_uri registered, use that instead 86 return redirectURIFromClient, nil 87 } 88 } else if redirectTo, ok := isMatchingRedirectURI(rawurl, client.GetRedirectURIs()); rawurl != "" && ok { 89 // If a redirect_uri was given and the clients knows it (simple string comparison!) 90 // return it. 91 if parsed, err := url.Parse(redirectTo); err == nil && IsValidRedirectURI(parsed) { 92 // If no redirect_uri was given and the client has exactly one valid redirect_uri registered, use that instead 93 return parsed, nil 94 } 95 } 96 97 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.")) 98 } 99 100 // Match a requested redirect URI against a pool of registered client URIs 101 // 102 // Test a given redirect URI against a pool of URIs provided by a registered client. 103 // If the OAuth 2.0 Client has loopback URIs registered either an IPv4 URI http://127.0.0.1 or 104 // an IPv6 URI http://[::1] a client is allowed to request a dynamic port and the server MUST accept 105 // it as a valid redirection uri. 106 // 107 // https://tools.ietf.org/html/rfc8252#section-7.3 108 // Native apps that are able to open a port on the loopback network 109 // interface without needing special permissions (typically, those on 110 // desktop operating systems) can use the loopback interface to receive 111 // the OAuth redirect. 112 // 113 // Loopback redirect URIs use the "http" scheme and are constructed with 114 // the loopback IP literal and whatever port the client is listening on. 115 func isMatchingRedirectURI(uri string, haystack []string) (string, bool) { 116 requested, err := url.Parse(uri) 117 if err != nil { 118 return "", false 119 } 120 121 for _, b := range haystack { 122 if b == uri { 123 return b, true 124 } else if isMatchingAsLoopback(requested, b) { 125 // We have to return the requested URL here because otherwise the port might get lost (see isMatchingAsLoopback) 126 // description. 127 return uri, true 128 } 129 } 130 return "", false 131 } 132 133 func isMatchingAsLoopback(requested *url.URL, registeredURI string) bool { 134 registered, err := url.Parse(registeredURI) 135 if err != nil { 136 return false 137 } 138 139 // Native apps that are able to open a port on the loopback network 140 // interface without needing special permissions (typically, those on 141 // desktop operating systems) can use the loopback interface to receive 142 // the OAuth redirect. 143 // 144 // Loopback redirect URIs use the "http" scheme and are constructed with 145 // the loopback IP literal and whatever port the client is listening on. 146 // 147 // Source: https://tools.ietf.org/html/rfc8252#section-7.3 148 if requested.Scheme == "http" && 149 isLoopbackAddress(requested.Host) && 150 registered.Hostname() == requested.Hostname() && 151 // The port is skipped here - see codedoc above! 152 registered.Path == requested.Path && 153 registered.RawQuery == requested.RawQuery { 154 return true 155 } 156 157 return false 158 } 159 160 // Check if address is either an IPv4 loopback or an IPv6 loopback- 161 // An optional port is ignored 162 func isLoopbackAddress(address string) bool { 163 match, _ := regexp.MatchString("^(127.0.0.1|\\[::1\\])(:?)(\\d*)$", address) 164 return match 165 } 166 167 // IsValidRedirectURI validates a redirect_uri as specified in: 168 // 169 // * https://tools.ietf.org/html/rfc6749#section-3.1.2 170 // * The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3. 171 // * The endpoint URI MUST NOT include a fragment component. 172 // * https://tools.ietf.org/html/rfc3986#section-4.3 173 // absolute-URI = scheme ":" hier-part [ "?" query ] 174 // * https://tools.ietf.org/html/rfc6819#section-5.1.1 175 func IsValidRedirectURI(redirectURI *url.URL) bool { 176 // We need to explicitly check for a scheme 177 if !govalidator.IsRequestURL(redirectURI.String()) { 178 return false 179 } 180 181 if redirectURI.Fragment != "" { 182 // "The endpoint URI MUST NOT include a fragment component." 183 return false 184 } 185 186 return true 187 } 188 189 func IsRedirectURISecure(redirectURI *url.URL) bool { 190 return !(redirectURI.Scheme == "http" && !IsLocalhost(redirectURI)) 191 } 192 193 // IsRedirectURISecureStrict is stricter than IsRedirectURISecure and it does not allow custom-scheme 194 // URLs because they can be hijacked for native apps. Use claimed HTTPS redirects instead. 195 // See discussion in https://github.com/ory/fosite/pull/489. 196 func IsRedirectURISecureStrict(redirectURI *url.URL) bool { 197 return redirectURI.Scheme == "https" || (redirectURI.Scheme == "http" && IsLocalhost(redirectURI)) 198 } 199 200 func IsLocalhost(redirectURI *url.URL) bool { 201 hn := redirectURI.Hostname() 202 return strings.HasSuffix(hn, ".localhost") || hn == "127.0.0.1" || hn == "::1" || hn == "localhost" 203 } 204 205 func WriteAuthorizeFormPostResponse(redirectURL string, parameters url.Values, template *template.Template, rw io.Writer) { 206 _ = template.Execute(rw, struct { 207 RedirURL string 208 Parameters url.Values 209 }{ 210 RedirURL: redirectURL, 211 Parameters: parameters, 212 }) 213 } 214 215 // Deprecated: Do not use. 216 func URLSetFragment(source *url.URL, fragment url.Values) { 217 var f string 218 for k, v := range fragment { 219 for _, vv := range v { 220 if len(f) != 0 { 221 f += fmt.Sprintf("&%s=%s", k, vv) 222 } else { 223 f += fmt.Sprintf("%s=%s", k, vv) 224 } 225 } 226 } 227 source.Fragment = f 228 } 229 230 func GetPostFormHTMLTemplate(f Fosite) *template.Template { 231 formPostHTMLTemplate := f.FormPostHTMLTemplate 232 if formPostHTMLTemplate == nil { 233 formPostHTMLTemplate = FormPostDefaultTemplate 234 } 235 return formPostHTMLTemplate 236 } 237