diff options
author | Mike Crute <mike@crute.us> | 2017-07-18 03:35:52 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-07-18 03:35:52 +0000 |
commit | 72f7d25a69b6ed41a15eef973f9fbb821931c901 (patch) | |
tree | fb6c7e60e273c7a471c10c4ab6236a58f1efe2d2 | |
download | py_openid_connect-72f7d25a69b6ed41a15eef973f9fbb821931c901.tar.bz2 py_openid_connect-72f7d25a69b6ed41a15eef973f9fbb821931c901.tar.xz py_openid_connect-72f7d25a69b6ed41a15eef973f9fbb821931c901.zip |
-rwxr-xr-x | manage.py | 64 | ||||
-rw-r--r-- | server.py | 87 | ||||
-rw-r--r-- | var/ssl-cert.pem | 17 | ||||
-rw-r--r-- | var/ssl-key.pem | 28 |
4 files changed, 196 insertions, 0 deletions
diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..8838a1e --- /dev/null +++ b/manage.py | |||
@@ -0,0 +1,64 @@ | |||
1 | #!/usr/bin/env python | ||
2 | |||
3 | import os | ||
4 | import sys | ||
5 | import socket | ||
6 | from OpenSSL import crypto | ||
7 | from OpenSSL.crypto import PKey, X509 | ||
8 | |||
9 | from server import app | ||
10 | |||
11 | |||
12 | VAR_PATH = "var" | ||
13 | LOG_PATH = os.path.join(VAR_PATH, "log") | ||
14 | KEY_PATH = os.path.join(VAR_PATH, "ssl-key.pem") | ||
15 | CERT_PATH = os.path.join(VAR_PATH, "ssl-cert.pem") | ||
16 | |||
17 | |||
18 | def generate_cert(serial=1): | ||
19 | ca = X509() | ||
20 | ca.set_version(2) | ||
21 | ca.set_serial_number(serial) | ||
22 | ca.get_subject().CN = socket.getfqdn() | ||
23 | ca.gmtime_adj_notBefore(0) | ||
24 | ca.gmtime_adj_notAfter(24 * 60 * 60) | ||
25 | ca.set_issuer(ca.get_subject()) | ||
26 | ca.set_pubkey(key) | ||
27 | ca.sign(key, "sha256") | ||
28 | |||
29 | with open(CERT_PATH, "wb") as fp: | ||
30 | fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca)) | ||
31 | |||
32 | |||
33 | if not os.path.exists(VAR_PATH): | ||
34 | os.mkdir(VAR_PATH) | ||
35 | |||
36 | if not os.path.exists(LOG_PATH): | ||
37 | os.mkdir(LOG_PATH) | ||
38 | |||
39 | if not os.path.exists(KEY_PATH): | ||
40 | key = PKey() | ||
41 | key.generate_key(crypto.TYPE_RSA, 2048) | ||
42 | |||
43 | with open(KEY_PATH, "wb") as fp: | ||
44 | os.chmod(KEY_PATH, 0o600) | ||
45 | fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) | ||
46 | else: | ||
47 | with open(KEY_PATH, "rb") as fp: | ||
48 | key = crypto.load_privatekey(crypto.FILETYPE_PEM, fp.read()) | ||
49 | |||
50 | |||
51 | if not os.path.exists(CERT_PATH): | ||
52 | generate_cert() | ||
53 | else: | ||
54 | with open(CERT_PATH, "rb") as fp: | ||
55 | cert = crypto.load_certificate(crypto.FILETYPE_PEM, fp.read()) | ||
56 | |||
57 | if cert.has_expired(): | ||
58 | generate_cert(cert.get_serial_number() + 1) | ||
59 | |||
60 | |||
61 | if __name__ == "__main__" and sys.argv[-1] == "runserver": | ||
62 | app.run(host="0.0.0.0", port=5000, | ||
63 | ssl_context=("var/ssl-cert.pem", "var/ssl-key.pem"), | ||
64 | threaded=True, debug=True) | ||
diff --git a/server.py b/server.py new file mode 100644 index 0000000..0bebe2a --- /dev/null +++ b/server.py | |||
@@ -0,0 +1,87 @@ | |||
1 | import flask | ||
2 | import urllib.parse | ||
3 | from flask import request | ||
4 | |||
5 | |||
6 | # Application Config | ||
7 | OIDC_ISSUER = "https://id.crute.me/" | ||
8 | OIDC_AUTH_ENDPOINT = "{}login".format(OIDC_ISSUER) | ||
9 | OIDC_JWKS_ENDPOINT = "{}jwks".format(OIDC_ISSUER) | ||
10 | DOMAIN_WHITELIST = ["crute.me", "crute.us"] | ||
11 | |||
12 | |||
13 | # Generic Constants | ||
14 | JRD_MIMETYPE = "application/jrd+json" | ||
15 | OIDC_ISSUER_QUERY = "http://openid.net/specs/connect/1.0/issuer" | ||
16 | JRD_404 = flask.Response("Not found", status=404, mimetype=JRD_MIMETYPE) | ||
17 | |||
18 | |||
19 | app = flask.Flask(__name__) | ||
20 | |||
21 | |||
22 | def parse_email_addr(addr): | ||
23 | try: | ||
24 | user, domain = addr.split("@") | ||
25 | return user, domain | ||
26 | except ValueError: | ||
27 | return None | ||
28 | |||
29 | |||
30 | @app.route("/.well-known/webfinger", methods=["GET"]) | ||
31 | def webfinger(): | ||
32 | resource = request.args.get("resource") | ||
33 | rel = request.args.get("rel") | ||
34 | |||
35 | # Only support OIDC queries | ||
36 | if not rel or not resource or not rel == OIDC_ISSUER_QUERY: | ||
37 | return JRD_404 | ||
38 | |||
39 | # Only support email address queries | ||
40 | acct = urllib.parse.urlparse(resource) | ||
41 | if acct.scheme != "acct": | ||
42 | return JRD_404 | ||
43 | |||
44 | # Ensure that the query is in a whitelisted domain | ||
45 | _, domain = parse_email_addr(acct.path) | ||
46 | if domain not in DOMAIN_WHITELIST: | ||
47 | return JRD_404 | ||
48 | |||
49 | # We don't validate that the user exists to prevent leaking information | ||
50 | # about what users exist; plus, it doesn't really matter since the querier | ||
51 | # only cares about the OIDC issuer endpoint which is user independent | ||
52 | res = flask.jsonify({ | ||
53 | "subject": resource, | ||
54 | "links": [{ | ||
55 | "rel": OIDC_ISSUER_QUERY, | ||
56 | "href": OIDC_ISSUER, | ||
57 | }] | ||
58 | }) | ||
59 | res.mimetype = JRD_MIMETYPE | ||
60 | return res | ||
61 | |||
62 | |||
63 | |||
64 | @app.route("/.well-known/openid-configuration", methods=["GET"]) | ||
65 | def openid_configuration(): | ||
66 | return flask.jsonify({ | ||
67 | "issuer": OIDC_ISSUER, | ||
68 | "authorization_endpoint": OIDC_AUTH_ENDPOINT, | ||
69 | "jwks_uri": OIDC_JWKS_ENDPOINT, | ||
70 | "scopes_supported": ["openid"], | ||
71 | "response_types_supported": ["id_token"], | ||
72 | "response_modes_supported": ["query"], | ||
73 | "grant_types_supported": ["implicit"], | ||
74 | "subject_types_supported": ["public"], | ||
75 | "id_token_signing_alg_values_supported": ["RS256"], | ||
76 | "acr_values_supported": ["want_mfa", "need_mfa"], | ||
77 | }) | ||
78 | |||
79 | |||
80 | @app.route("/login", methods=["GET", "POST"]) | ||
81 | def login(): | ||
82 | pass | ||
83 | |||
84 | |||
85 | @app.route("/jwks", methods=["GET"]) | ||
86 | def keys(): | ||
87 | pass | ||
diff --git a/var/ssl-cert.pem b/var/ssl-cert.pem new file mode 100644 index 0000000..207aea1 --- /dev/null +++ b/var/ssl-cert.pem | |||
@@ -0,0 +1,17 @@ | |||
1 | -----BEGIN CERTIFICATE----- | ||
2 | MIICwTCCAamgAwIBAgIBATANBgkqhkiG9w0BAQsFADAkMSIwIAYDVQQDDBltY3J1 | ||
3 | dGUtdmlydC5zZWExLmNydXRlLm1lMB4XDTE3MDIyNTAzNTAwNloXDTE3MDIyNjAz | ||
4 | NTAwNlowJDEiMCAGA1UEAwwZbWNydXRlLXZpcnQuc2VhMS5jcnV0ZS5tZTCCASIw | ||
5 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZqUzkQdVMdNX0ozFb1BF2SXJao | ||
6 | oDHs1gz4yvNTpfCMVKl+DIpb6QOkfEL4UOs2MSRGm0we2Va7e87gnmRGdmjLuSQQ | ||
7 | SDEVCnGH5eqL9WqqZzMB0TJsHm1saeJakGePVeDiJMlRFxCjS/8TXBkRP0LJge6i | ||
8 | TQQ0ZVbAVxEvmuGRdPIIQGqBifKm6zaGszD992xARt8vYDiAlGKEssmLnrTpZ1kp | ||
9 | dc/r+tKe+FHWVLggEvP9xkWVmTsABGd00CvYfI/716BZrG7dpQrRfMGFOcq3lpPA | ||
10 | uIp+uHaOVmpT85GlJl/LM6qbnjh3xL9iJFFgdIWVZD6USlWTlVaFm1l8os8CAwEA | ||
11 | ATANBgkqhkiG9w0BAQsFAAOCAQEAnwpJAp2TM7EeW/o3656dmmr35A5txEpCMJ16 | ||
12 | Mb5izuj67lrgcCqGltFRxZSyFRDii3/BN2SMGt2Uhtt/cctDJwU1Bl3Tt4WfsGgc | ||
13 | pLARspitlVEbpeOwoZdXj1m6ILHuQ2qsuTNBwKqW8ZmeaxKodPDWB9L9tCWypJSz | ||
14 | GvWAOvTqVqDJ3ob+UcCNHBH2on8Y2K14fCqcTsEFQs3vcPKfqBPKGs8xv7cZ+mrl | ||
15 | Wh++JI8GprNcPMF6owMGKhwVn/bXmDPXIb67OM05PvlfmbRs8DQEzzz5oQj7yLY7 | ||
16 | iI7zu/OBHprWeUvQcvZcWzWaeVp2a89X4t9O3ODa5QZmDELHNw== | ||
17 | -----END CERTIFICATE----- | ||
diff --git a/var/ssl-key.pem b/var/ssl-key.pem new file mode 100644 index 0000000..7cd8796 --- /dev/null +++ b/var/ssl-key.pem | |||
@@ -0,0 +1,28 @@ | |||
1 | -----BEGIN PRIVATE KEY----- | ||
2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmalM5EHVTHTV9 | ||
3 | KMxW9QRdklyWqKAx7NYM+MrzU6XwjFSpfgyKW+kDpHxC+FDrNjEkRptMHtlWu3vO | ||
4 | 4J5kRnZoy7kkEEgxFQpxh+Xqi/VqqmczAdEybB5tbGniWpBnj1Xg4iTJURcQo0v/ | ||
5 | E1wZET9CyYHuok0ENGVWwFcRL5rhkXTyCEBqgYnypus2hrMw/fdsQEbfL2A4gJRi | ||
6 | hLLJi5606WdZKXXP6/rSnvhR1lS4IBLz/cZFlZk7AARndNAr2HyP+9egWaxu3aUK | ||
7 | 0XzBhTnKt5aTwLiKfrh2jlZqU/ORpSZfyzOqm544d8S/YiRRYHSFlWQ+lEpVk5VW | ||
8 | hZtZfKLPAgMBAAECggEAcMlFCDrYWXFFbEM3YoQC5mwo5k74631JgVcpLYr2vzZM | ||
9 | tubgFvG91iMnuLfVF+UNnzlfTVLnGDpO6eIgV3POEq5oF0IVu3Y4MsPZFoKu3REK | ||
10 | im5j2xmd8al1hdy9XAKwQI7kQbD8weD6w7DaTX778gbiUdqb+gqM2CPZnqM2BEPZ | ||
11 | 8mDyQ9YteDykbC/ZWDEnorb35F9zKp4BFBOgLpRm7Ylj9VYqYxutj94fAA8juKyu | ||
12 | cB8XntnDZE40VSY6hPhivmF3Bi8OcHBnKNuVW/+Cg8SqbvuxeMRN+lgV8AcRvQFR | ||
13 | eFE0WRo0uEpkwsoFwXW5jdOyPYzeNJNLcjKBNhdz6QKBgQDYwBdQZIW3iGLjiEtY | ||
14 | j5naThzm2QyIcV5TolIOE2iFT0c7ZpM2r9k5iWGbN1BVJYp3A4WI4Pf9TdpA3upQ | ||
15 | nGnm7e2TF2pbZmb7dNCV+8SkHxqE0TmZAfX2zHVYa62Ak+4/CaN45CR+3NLpmc7E | ||
16 | 4A9JqshcLyJpIgJpXohpSa3KkwKBgQDEjNmzcxpDZson3v1wVfRpREE/+ImztwMb | ||
17 | wQeMO4IdQ8PVcaTS/9Tfo72ttjJEQJKJWOQK/DrJJB3tbTfv7OxXW+9g40uiNb4y | ||
18 | P/1Zj9osAWQ9US5SR2Diu2TJf+RnLOONHDFlolp6h2ODp6eZitRedVrlNOhIKjRE | ||
19 | 0X6YvHIgVQKBgHhmG404iV5tkCC7sL685cVx5nQE1QVlk/P5EoNnHIQQiYzhaZzT | ||
20 | CWMAgQtrKmBhvgxmFGL4NEisWjP2n0mv2NPCDlnKg/XJaCL9vG/TrmNMM7rbTZdE | ||
21 | k5BH+lbnWTU29vxlKj0Y3XzqxO2l1kld/J/7EdjDBeZIUfad4EA8ASGpAoGAcLbR | ||
22 | VY5pfkOhJJJFRrmUKxXgUV4nIa+PEch1mp65tCybTnkYa4QYwJN2//pfAzMAldEr | ||
23 | HX2d/KFApFqg3G9C7aokMazHP+OQCeMWV9kd5WR65or6hGa4ke2jE8bK5bwhVlfX | ||
24 | oBAl0OR5VhO8ElpCBVVJZe4cUt4ZEWoOLmrLC+0CgYBLDlBZsvfE1x4UEn4LFLQC | ||
25 | LnNEjFvYbtKXP4Tm5Kk3FhDUMoBkwm69AhXgfq+ddCXUrBjsMaYZhdi2FPqa5qEh | ||
26 | K6RPu42yIElN50UnabRkZbShCUEEeioOGTjAjhyd77O9ol9C7M57L38OnEs8MmRa | ||
27 | gvVNvuCVuHjcnd06dDTWPw== | ||
28 | -----END PRIVATE KEY----- | ||