Skip to content

Commit

Permalink
Properly handle protected endpoints with query strings.
Browse files Browse the repository at this point in the history
To facilitate nginx configuration, we've changed the redirect from
https://$http_host/saml/login?url=$request_uri to
https://$http_host/saml/login$request_uri. I erroneously thought $request_uri
would be encoded, but it wasn't. The old way created a potentially invalid uri.
The new way preserves the original url that we want to come back to.
  • Loading branch information
jeffFranklin committed Mar 9, 2019
1 parent 41d812e commit e406790
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 16 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ URL, the proxy will take care of the rest.
Example nginx.conf would look like the following...

```
location /secure {
auth_request /saml/status/group/uw_example_group;
location / {
auth_request /saml/status/group/uw_it_all;
auth_request_set $auth_user $upstream_http_x_saml_user;
error_page 401 = @login_required;
proxy_set_header Remote-User $auth_user;
proxy_pass http://secure:5000/;
}
Expand All @@ -18,21 +19,23 @@ location /saml/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Prefix /saml;
proxy_set_header X-Saml-Entity-Id https://samldemo.iamdev.s.uw.edu/saml;
# acs - post-back url registered with the IdP.
proxy_set_header X-Saml-Acs /saml/login;
proxy_pass http://saml:5000/;
}
location @error401 {
return 302 https://$http_host/saml/login?url=$request_uri;
location @login_required {
return 302 https://$http_host/saml/login$request_uri;
}
```

See the [example nginx config](test/nginx/server.conf) for more examples.

## SECRET_KEY

This app wants an environment variable `SECRET_KEY`, which should be a secure,
randomly-generated string. Otherwise, we generate one on the fly, which only
works long as the app is running, and won't work in a distributed environment.
works as long as the app is running, and won't work in a distributed environment.
SECRET_KEY is used to sign cookies, so setting a new key effectively
invalidates all existing sessions.
45 changes: 35 additions & 10 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,58 @@ def status(group=None):
403 if a group was requested that the user is not a member of, or 200
if the user is authenticated.
group - a UW Group the user must be a member of.
group - a UW Group the user must be a member of. An SP must be registered
to receive that group.
"""
userid = session.get('userid')
groups = session.get('groups', [])
if not userid:
abort(401)
if group and group not in groups:
message = f"{userid} not a member of {group} or SP can't receive it"
app.logger.error(message)
abort(403)
headers = {'X-Saml-User': userid,
'X-Saml-Groups': ':'.join(groups)}
txt = f'Logged in as: {userid}\nGroups: {str(groups)}'
return Response(txt, status=200, headers=headers)


def _saml_args():
"""Get entity_id and acs_url from request.headers."""
return {
'entity_id': request.headers['X-Saml-Entity-Id'],
'acs_url': urljoin(request.url_root, request.headers['X-Saml-Acs'])
}


@app.route('/login/')
@app.route('/login/<path:return_to>')
def login_redirect(return_to=''):
"""
Redirect to the IdP for SAML initiation.
return_to - the path to redirect back to after authentication. This and
the request.query_string are set on the SAML RelayState.
"""
query_string = '?' + request.query_string.decode()
if query_string == '?':
query_string = ''
return_to = f'/{return_to}{query_string}'
args = _saml_args()
return redirect(uw_saml2.login_redirect(return_to=return_to, **args))


@app.route('/login', methods=['GET', 'POST'])
def login():
"""
Return a SAML Request redirect, or process a SAML Response, depending
on whether GET or POST.
Process a SAML Response, and set the uwnetid and groups on the session.
"""
session.clear()
args = {
'entity_id': request.headers['X-Saml-Entity-Id'],
'acs_url': urljoin(request.url_root, request.headers['X-Saml-Acs'])
}
if request.method == 'GET':
args['return_to'] = request.args.get('url', None)
return redirect(uw_saml2.login_redirect(**args))
return login_redirect()

args = _saml_args()
attributes = uw_saml2.process_response(request.form, **args)

session['userid'] = attributes['uwnetid']
Expand All @@ -80,5 +103,7 @@ def logout():
def healthz():
"""Return a 200 along with some useful links."""
return '''
<p><a href="login">Sign in</a></p><p><a href="logout">Logout</a></p>
<p><a href="login">Sign in</a></p>
<p><a href="status">Status</a></p>
<p><a href="logout">Logout</a></p>
'''
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '3'
services:
saml:
build: .
image: nginx-saml-proxy
volumes:
- ./app.py:/app/app.py
nginx:
build: test/nginx
image: mynginx
ports: ["443:443"]
volumes:
- ./test/nginx/server.conf:/etc/nginx/conf.d/server.conf
17 changes: 17 additions & 0 deletions test/nginx/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM nginx as certbuilder

RUN apt-get update && apt-get install -y ssl-cert && apt-get clean
RUN mkdir /ssl && \
cp -p /etc/ssl/private/ssl-cert-snakeoil.key /ssl/key.pem && \
cp -p /etc/ssl/certs/ssl-cert-snakeoil.pem /ssl/cert.pem

FROM nginx

EXPOSE 80
EXPOSE 443

RUN rm /etc/nginx/conf.d/* && mkdir /static
COPY server.conf /etc/nginx/conf.d/server.conf
COPY --from=certbuilder /ssl /etc/nginx/ssl

CMD ["nginx", "-g", "daemon off;"]
33 changes: 33 additions & 0 deletions test/nginx/server.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
root /usr/share/nginx/html;

# anyone with a UW NetID can access this
location / {
auth_request /saml/status;
auth_request_set $auth_user $upstream_http_x_saml_user;
error_page 401 = @login_required;
}

# user must be a member of uw_it_all
location /secure {
auth_request /saml/status/group/uw_it_all;
error_page 401 = @login_required;
alias /usr/share/nginx/html;
}

location /saml/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Saml-Entity-Id https://samldemo.iamdev.s.uw.edu/saml;
proxy_set_header X-Saml-Acs /saml/login;
proxy_pass http://saml:5000/;
}

location @login_required {
return 302 https://$http_host/saml/login$request_uri;
}
}

0 comments on commit e406790

Please sign in to comment.