diff --git a/README.md b/README.md index 9ff6073..4947c5a 100644 --- a/README.md +++ b/README.md @@ -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/; } @@ -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. diff --git a/app.py b/app.py index 3e805e8..3ca8794 100644 --- a/app.py +++ b/app.py @@ -29,13 +29,16 @@ 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)} @@ -43,21 +46,41 @@ def status(group=None): 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/') +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'] @@ -80,5 +103,7 @@ def logout(): def healthz(): """Return a 200 along with some useful links.""" return ''' -

Sign in

Logout

+

Sign in

+

Status

+

Logout

''' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7dc9275 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/test/nginx/Dockerfile b/test/nginx/Dockerfile new file mode 100644 index 0000000..8f43d19 --- /dev/null +++ b/test/nginx/Dockerfile @@ -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;"] \ No newline at end of file diff --git a/test/nginx/server.conf b/test/nginx/server.conf new file mode 100644 index 0000000..4fb9435 --- /dev/null +++ b/test/nginx/server.conf @@ -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; + } +}