0

I have an App Engine service with a few methods implemented, where I restrict all routes with the login: admin option in the app.yaml.

Making a POST request to my service works:

fetch('http://localhost:8081/api/foo', {
  credentials: 'include'});

But making a PUT request fails

await fetch('http://localhost:8081/api/foo', {
  credentials: 'include',
  method: 'PUT',
  body: 'hi there'});

with the following error:

Response to preflight request doesn't pass access control check:
Redirect is not allowed for a preflight request.

I understand this is because my request is somehow not authenticated, and the server redirects my request to the login page. What I don't understand is how to authenticate it.

I'm using webapp2 to process the requests, and setting the following headers:

self.response.headers['Access-Control-Allow-Credentials'] = 'true'
self.response.headers['Content-Type'] = 'application/json'
# This feels wrong, but I still don't clearly understand what this header's purpose is...
self.response.headers['Access-Control-Allow-Origin'] = self.request.headers['Origin']

I think the deeper problem is that I don't undestand how this login feature works (is it cookie based? Why does it work with GET but not PUT? ...), and I don't truly understand CORS either.

Thanks for any help!

sideshowbarker
  • 62,215
  • 21
  • 143
  • 153
bastien girschig
  • 481
  • 3
  • 17

2 Answers2

1

So, after discussing with Dan Cornilescu, here is the solution I came up with (Thanks Dan!)

Instead of having my classes inherit webapp2.RequestHandler, they inherit this custom HandlerWrapper. The big difference is that when receiving an 'OPTIONS' request (ie. preflight), there is no login required. This is what was causing my problem: I couldn't get the preflight request to be authenticated, so now it doesn't need to be.

The CORS is also handled there, with a list of allowed origins

class HandlerWrapper(webapp2.RequestHandler):
  def __init__(self, request, response):
    super(HandlerWrapper, self).__init__(request, response)

    self.allowed_origins = [
      r'http://localhost(:\d{2,})?$', # localhost on any port
      r'https://\w+-dot-myproject.appspot.com' # all services in the app engine project
    ]
    self.allowed_methods = 'GET, PUT, POST, OPTIONS'
    self.content_type = 'application/json'
    # login mode: either 'admin', 'user', or 'public'
    self.login = 'admin'

  def dispatch(self):
    # set the Allow-Origin header.
    if self.request.headers.has_key('origin') and match_origin(self.request.headers['Origin'], self.allowed_origins):
      self.response.headers['Access-Control-Allow-Origin'] = self.request.headers['Origin']

    # set other headers
    self.response.headers['Access-Control-Allow-Methods'] = self.allowed_methods
    self.response.headers['Content-Type'] = 'application/json'
    self.response.headers['Access-Control-Allow-Credentials'] = 'true'

    # Handle preflight requests: Never require a login.
    if self.request.method == "OPTIONS":
      # For some reason, the following line raises a '405 (Method Not Allowed)'
      # error, so we just skip the dispatch and it works.
      # super(HandlerWrapper, self).dispatch()
      return

    # Handle regular requests
    user = users.get_current_user()
    if self.login == 'admin' and not users.is_current_user_admin():
      self.abort(403)
    elif self.login == 'user' and not user:
      self.abort(403)
    else:
      super(HandlerWrapper, self).dispatch()

def match_origin(origin, allowed_origins):
  for pattern in allowed_origins:
    if re.match(pattern, origin): return True
  return False
bastien girschig
  • 481
  • 3
  • 17
0

The login: admin configuration is based on the Users API, available only in the 1st generation standard environment. Not a CORS problem. From the login row in the handlers element table:

When a URL handler with a login setting other than optional matches a URL, the handler first checks whether the user has signed in to the application using its authentication option. If not, by default, the user is redirected to the sign-in page. You can also use auth_fail_action to configure the app to simply reject requests for a handler from users who are not properly authenticated, instead of redirecting the user to the sign-in page.

To use the Users API the user must literally login before the PUT request is made. Make a GET request first, which will redirect you to the login page, execute the login, then make the PUT request.

If that's not something you can achieve then you need to use a different authentication mechanism, not the one based on login: admin.

Update:

The above is true, but rather unrelated as the Users API authentication is addressed - you did mention that some other request method to the same URL works.

The error you get is indeed CORS-related, see Response to preflight request doesn't pass access control check. But I'd suggest not focusing on the accepted answer (which is only about working around CORS), but rather on this one, which is about doing CORS correctly.

Dan Cornilescu
  • 37,297
  • 11
  • 54
  • 89
  • Thanks... but I still don't quite understand: I'm making the request while the client is already logged in, and It works with a GET request. I guess this is related to the GET request not needing a preflight request, but I don't know how I can authenticate the preflight with the users API (currently, it's getting redirected to the login page) – bastien girschig Mar 17 '19 at 14:57
  • I see, my answer is off - you do get a CORS-related error ;) I'll update. – Dan Cornilescu Mar 17 '19 at 15:03
  • I have managed to go around the problem by disabling the login:admin option and instead using users.is_current_user_admin() in my handlers, except for the 'options' handler, where I only set some headers. I don't like manually checking credentials: it feels very error-prone. I'd much rather find a solution to make that preflight request work with the "auth: admin" option. – bastien girschig Mar 17 '19 at 15:19
  • That's rather easy: just create a base class for all your handlers, implementing the actual `()` functions (i.e. `get()` & friends) which would enforce the check before calling `self.handle_()`. In the child classes you'd implement just the `handle_()` functions instead of the `()` ones. – Dan Cornilescu Mar 17 '19 at 15:34
  • That makes sense... Thanks! I'll put all of that together, make sure everything works, and accept your answer. – bastien girschig Mar 17 '19 at 15:41