...

Source file src/github.com/ory/fosite/authorize_helper.go

Documentation: github.com/ory/fosite

     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  

View as plain text