diff --git a/.gitignore b/.gitignore index e23adc2d6..ebcd91210 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ docs/research/data/** public/ credentials* +*.pem +*.crt + +*.bk + ### The usual garbage files ### # Byte-compiled / optimized / DLL files diff --git a/ops/manifests/manifest-staging.yaml b/ops/manifests/manifest-staging.yaml index e474f8ba2..4e92cbcc5 100644 --- a/ops/manifests/manifest-staging.yaml +++ b/ops/manifests/manifest-staging.yaml @@ -8,7 +8,7 @@ applications: memory: 512M stack: cflinuxfs3 timeout: 180 - command: gunicorn registrar.config.wsgi -t 60 + command: ./run.sh health-check-type: http health-check-http-endpoint: /health env: @@ -16,6 +16,10 @@ applications: PYTHONUNBUFFERED: yup # Tell Django where to find its configuration DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: getgov-staging.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO routes: - route: getgov-staging.app.cloud.gov services: diff --git a/ops/manifests/manifest-unstable.yaml b/ops/manifests/manifest-unstable.yaml index 4b523118c..88c475b0a 100644 --- a/ops/manifests/manifest-unstable.yaml +++ b/ops/manifests/manifest-unstable.yaml @@ -16,6 +16,10 @@ applications: PYTHONUNBUFFERED: yup # Tell Django where to find its configuration DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: getgov-unstable.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO routes: - route: getgov-unstable.app.cloud.gov services: diff --git a/src/Pipfile b/src/Pipfile index 0258dc930..1c81e1a00 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -6,10 +6,13 @@ name = "pypi" [packages] django = "*" cfenv = "*" +pycryptodomex = "*" django-allow-cidr = "*" django-csp = "*" environs = {extras=["django"]} gunicorn = "*" +oic = "*" +pyjwkest = "*" psycopg2-binary = "*" whitenoise = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 4b9a11c9b..1f80d0797 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a699f5429c7a64d18840baefdfc9731d7c73d57ed55fd701bcc411efeb6e4035" + "sha256": "45645e181d935b55c0d58e163245cce64c11cfbcc86ad9cbb2d549d613d9069e" }, "pipfile-spec": 6, "requires": {}, @@ -22,6 +22,20 @@ "markers": "python_version >= '3.7'", "version": "==3.5.2" }, + "beaker": { + "hashes": [ + "sha256:ad5d1c05027ee3be3a482ea39f8cb70339b41e5d6ace0cb861382754076d187e" + ], + "version": "==1.11.0" + }, + "certifi": { + "hashes": [ + "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5", + "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516" + ], + "markers": "python_version >= '3.6'", + "version": "==2022.9.14" + }, "cfenv": { "hashes": [ "sha256:7815bffcc4a3db350f92517157fafc577c11b5a7ff172dc5632f1042b93073e8", @@ -30,6 +44,123 @@ "index": "pypi", "version": "==0.5.3" }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_version >= '3.6'", + "version": "==2.1.1" + }, + "cryptography": { + "hashes": [ + "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", + "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", + "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", + "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", + "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", + "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", + "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", + "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", + "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", + "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", + "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", + "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", + "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", + "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", + "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", + "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", + "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", + "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", + "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", + "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", + "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", + "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", + "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", + "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", + "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", + "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" + ], + "markers": "python_version >= '3.6'", + "version": "==38.0.1" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.7.1" + }, "dj-database-url": { "hashes": [ "sha256:ccf3e8718f75ddd147a1e212fca88eecdaa721759ee48e38b485481c77bca3dc", @@ -93,6 +224,13 @@ ], "version": "==2.1.3" }, + "future": { + "hashes": [ + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.18.2" + }, "gunicorn": { "hashes": [ "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", @@ -101,6 +239,68 @@ "index": "pypi", "version": "==20.1.0" }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "mako": { + "hashes": [ + "sha256:3724869b363ba630a272a5f89f68c070352137b8fd1757650017b7e06fda163f", + "sha256:8efcb8004681b5f71d09c983ad5a9e6f5c40601a6ec469148753292abc0da534" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.2" + }, + "markupsafe": { + "hashes": [ + "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", + "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", + "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", + "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", + "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", + "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", + "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", + "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", + "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", + "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", + "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", + "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", + "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", + "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", + "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", + "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", + "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", + "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", + "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", + "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", + "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", + "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", + "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", + "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", + "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", + "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", + "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", + "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", + "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", + "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", + "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", + "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", + "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", + "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", + "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", + "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", + "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", + "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", + "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", + "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.1" + }, "marshmallow": { "hashes": [ "sha256:1172ce82765bf26c24a3f9299ed6dbeeca4d213f638eaa39a37772656d7ce408", @@ -109,6 +309,14 @@ "markers": "python_version >= '3.7'", "version": "==3.17.1" }, + "oic": { + "hashes": [ + "sha256:b82316c4b9633781b8fcb091a7d082ffc863f850a87d8725ead454746aeae677", + "sha256:c1a46dd5f803349f1eea7393d70a3f2bdbc97e73b96f3ebb54843e1dc190f5e4" + ], + "index": "pypi", + "version": "==1.4.0" + }, "orderedmultidict": { "hashes": [ "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", @@ -186,6 +394,56 @@ "index": "pypi", "version": "==2.9.3" }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pycryptodomex": { + "hashes": [ + "sha256:04cc393045a8f19dd110c975e30f38ed7ab3faf21ede415ea67afebd95a22380", + "sha256:0776bfaf2c48154ab54ea45392847c1283d2fcf64e232e85565f858baedfc1fa", + "sha256:0fadb9f7fa3150577800eef35f62a8a24b9ddf1563ff060d9bd3af22d3952c8c", + "sha256:18e2ab4813883ae63396c0ffe50b13554b32bb69ec56f0afaf052e7a7ae0d55b", + "sha256:191e73bc84a8064ad1874dba0ebadedd7cce4dedee998549518f2c74a003b2e1", + "sha256:35a8f7afe1867118330e2e0e0bf759c409e28557fb1fc2fbb1c6c937297dbe9a", + "sha256:3709f13ca3852b0b07fc04a2c03b379189232b24007c466be0f605dd4723e9d4", + "sha256:4540904c09704b6f831059c0dfb38584acb82cb97b0125cd52688c1f1e3fffa6", + "sha256:463119d7d22d0fc04a0f9122e9d3e6121c6648bcb12a052b51bd1eed1b996aa2", + "sha256:46b3f05f2f7ac7841053da4e0f69616929ca3c42f238c405f6c3df7759ad2780", + "sha256:48697790203909fab02a33226fda546604f4e2653f9d47bc5d3eb40879fa7c64", + "sha256:5676a132169a1c1a3712edf25250722ebc8c9102aa9abd814df063ca8362454f", + "sha256:65204412d0c6a8e3c41e21e93a5e6054a74fea501afa03046a388cf042e3377a", + "sha256:67e1e6a92151023ccdfcfbc0afb3314ad30080793b4c27956ea06ab1fb9bcd8a", + "sha256:6f5b6ba8aefd624834bc177a2ac292734996bb030f9d1b388e7504103b6fcddf", + "sha256:7341f1bb2dadb0d1a0047f34c3a58208a92423cdbd3244d998e4b28df5eac0ed", + "sha256:78d9621cf0ea35abf2d38fa2ca6d0634eab6c991a78373498ab149953787e5e5", + "sha256:8eecdf9cdc7343001d047f951b9cc805cd68cb6cd77b20ea46af5bffc5bd3dfb", + "sha256:94c7b60e1f52e1a87715571327baea0733708ab4723346598beca4a3b6879794", + "sha256:996e1ba717077ce1e6d4849af7a1426f38b07b3d173b879e27d5e26d2e958beb", + "sha256:a07a64709e366c2041cd5cfbca592b43998bf4df88f7b0ca73dca37071ccf1bd", + "sha256:b6306403228edde6e289f626a3908a2f7f67c344e712cf7c0a508bab3ad9e381", + "sha256:b9279adc16e4b0f590ceff581f53a80179b02cba9056010d733eb4196134a870", + "sha256:c4cb9cb492ea7dcdf222a8d19a1d09002798ea516aeae8877245206d27326d86", + "sha256:dd452a5af7014e866206d41751886c9b4bf379a339fdf2dbfc7dd16c0fb4f8e0", + "sha256:e2b12968522a0358b8917fc7b28865acac002f02f4c4c6020fcb264d76bfd06d", + "sha256:e3164a18348bd53c69b4435ebfb4ac8a4076291ffa2a70b54f0c4b80c7834b1d", + "sha256:e47bf8776a7e15576887f04314f5228c6527b99946e6638cf2f16da56d260cab", + "sha256:f8be976cec59b11f011f790b88aca67b4ea2bd286578d0bd3e31bcd19afcd3e4", + "sha256:fc9bc7a9b79fe5c750fc81a307052f8daabb709bdaabb0fb18fb136b66b653b5" + ], + "index": "pypi", + "version": "==3.15.0" + }, + "pyjwkest": { + "hashes": [ + "sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222" + ], + "index": "pypi", + "version": "==1.4.2" + }, "pyparsing": { "hashes": [ "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", @@ -202,6 +460,14 @@ "markers": "python_version >= '3.7'", "version": "==0.21.0" }, + "requests": { + "hashes": [ + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + ], + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.1" + }, "setuptools": { "hashes": [ "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", @@ -226,6 +492,22 @@ "markers": "python_version >= '3.5'", "version": "==0.4.2" }, + "typing-extensions": { + "hashes": [ + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.3.0" + }, + "urllib3": { + "hashes": [ + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" + }, "whitenoise": { "hashes": [ "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2", @@ -391,11 +673,11 @@ }, "pathspec": { "hashes": [ - "sha256:01eecd304ba0e6eeed188ae5fa568e99ef10265af7fd9ab737d6412b4ee0ab85", - "sha256:aefa80ac32d5bf1f96139dca67cefb69a431beff4e6bf1168468f37d7ab87015" + "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", + "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" ], "markers": "python_version >= '3.7'", - "version": "==0.10.0" + "version": "==0.10.1" }, "pbr": { "hashes": [ @@ -431,6 +713,7 @@ }, "pyyaml": { "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", @@ -442,26 +725,32 @@ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], @@ -510,18 +799,18 @@ }, "types-requests": { "hashes": [ - "sha256:86cb66d3de2f53eac5c09adc42cf6547eefbd0c7e1210beca1ee751c35d96083", - "sha256:feaf581bd580497a47fe845d506fa3b91b484cf706ff27774e87659837de9962" + "sha256:45b485725ed58752f2b23461252f1c1ad9205b884a1e35f786bb295525a3e16a", + "sha256:97d8f40aa1ffe1e58c3726c77d63c182daea9a72d9f1fa2cafdea756b2a19f2c" ], "index": "pypi", - "version": "==2.28.9" + "version": "==2.28.10" }, "types-urllib3": { "hashes": [ - "sha256:333e675b188a1c1fd980b4b352f9e40572413a4c1ac689c23cd546e96310070a", - "sha256:b78e819f0e350221d0689a5666162e467ba3910737bafda14b5c2c85e9bb1e56" + "sha256:a1b3aaea7dda3eb1b51699ee723aadd235488e4dc4648e030f09bc429ecff42f", + "sha256:cf7918503d02d3576e503bbfb419b0e047c4617653bba09624756ab7175e15c9" ], - "version": "==1.26.23" + "version": "==1.26.24" }, "typing-extensions": { "hashes": [ diff --git a/src/djangooidc/LICENSE b/src/djangooidc/LICENSE new file mode 100644 index 000000000..36ce8a107 --- /dev/null +++ b/src/djangooidc/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Andrea Biancini + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/djangooidc/NOTICE b/src/djangooidc/NOTICE new file mode 100644 index 000000000..f7b309d82 --- /dev/null +++ b/src/djangooidc/NOTICE @@ -0,0 +1,10 @@ +Python module to integrate pyoic into django +Copyright 2014 Andrea Biancini + +This product includes software developed by +Andrea Biancini + +Portions of this software is derived from the pyoidc project +avaialble from + https://github.com/rohe/pyoidc + diff --git a/src/djangooidc/README.rst b/src/djangooidc/README.rst new file mode 100644 index 000000000..41c79f04f --- /dev/null +++ b/src/djangooidc/README.rst @@ -0,0 +1,30 @@ +Django OpenID Connect (OIDC) authentication provider +==================================================== + +This module makes it easy to integrate OpenID Connect as an authentication source in a Django project. + +Behind the scenes, it uses Roland Hedberg's great pyoidc library. + +Modified by JHUAPL BOSS to support Python3 + +Modified by Thomas Frössman with fixes and additional modifications. + +A note for anyone viewing this file from the .gov repository: + +This code has been included from its upstream counterpart in order to minimize external dependencies. Here is an excerpt from setup.py:: + + name='django-oidc-tf', + description="""A Django OpenID Connect (OIDC) authentication backend""", + author='Thomas Frössman', + author_email='thomasf@jossystem.se', + url='https://github.com/py-pa/django-oidc', + packages=[ + 'djangooidc', + ], + include_package_data=True, + install_requires=[ + 'django>=1.10', + 'oic>=0.10.0', + ], + +It was taken from https://github.com/koriaf/django-oidc at ae4a0ba5e6bfda1495f9447a507e6f54cc056980. diff --git a/src/djangooidc/__init__.py b/src/djangooidc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py new file mode 100644 index 000000000..52cbc712b --- /dev/null +++ b/src/djangooidc/backends.py @@ -0,0 +1,77 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import logging + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend +from django.utils import timezone + +logger = logging.getLogger(__name__) + + +class OpenIdConnectBackend(ModelBackend): + """ + This backend checks a previously performed OIDC authentication. + If it is OK and the user already exists in the database, it is returned. + If it is OK and user does not exist in the database, it is created and + returned unless setting OIDC_CREATE_UNKNOWN_USER is False. + In all other cases, None is returned. + """ + + def authenticate(self, request, **kwargs): + logger.debug("kwargs %s" % kwargs) + user = None + if not kwargs or "sub" not in kwargs.keys(): + return user + + UserModel = get_user_model() + username = self.clean_username(kwargs["sub"]) + if "upn" in kwargs.keys(): + username = kwargs["upn"] + + # Some OP may actually choose to withhold some information, so we must + # test if it is present + openid_data = {"last_login": timezone.now()} + openid_data["first_name"] = kwargs.get("first_name", "") + openid_data["first_name"] = kwargs.get("given_name", "") + openid_data["first_name"] = kwargs.get("christian_name", "") + openid_data["last_name"] = kwargs.get("family_name", "") + openid_data["last_name"] = kwargs.get("last_name", "") + openid_data["email"] = kwargs.get("email", "") + + # Note that this could be accomplished in one try-except clause, but + # instead we use get_or_create when creating unknown users since it has + # built-in safeguards for multiple threads. + if getattr(settings, "OIDC_CREATE_UNKNOWN_USER", True): + args = { + UserModel.USERNAME_FIELD: username, + "defaults": openid_data, + } + user, created = UserModel.objects.update_or_create(**args) + if created: + user = self.configure_user(user, **kwargs) + else: + try: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: + try: + user = UserModel.objects.get(email=kwargs["email"]) + except UserModel.DoesNotExist: + return None + return user + + def clean_username(self, username): + """ + Performs any cleaning on the "username" prior to using it to get or + create the user object. Returns the cleaned username. + """ + return username + + def configure_user(self, user, **kwargs): + """ + Configures a user after creation and returns the updated user. + """ + user.set_unusable_password() + return user diff --git a/src/djangooidc/exceptions.py b/src/djangooidc/exceptions.py new file mode 100644 index 000000000..260750a4d --- /dev/null +++ b/src/djangooidc/exceptions.py @@ -0,0 +1,42 @@ +from oic import rndstr +from http import HTTPStatus as status + + +class OIDCException(Exception): + """ + Base class for django oidc exceptions. + Subclasses should provide `.status` and `.friendly_message` properties. + `.locator`, if used, should be a useful, unique identifier for + locating related log messages. + """ + + status = status.INTERNAL_SERVER_ERROR + friendly_message = "A server error occurred." + locator = None + + def __init__(self, friendly_message=None, status=None, locator=None): + if friendly_message is not None: + self.friendly_message = friendly_message + if status is not None: + self.status = status + if locator is not None: + self.locator = locator + else: + self.locator = rndstr(size=12) + + def __str__(self): + return f"[{self.locator}] {self.friendly_message}" + + +class AuthenticationFailed(OIDCException): + status = status.UNAUTHORIZED + friendly_message = "This login attempt didn't work." + + +class InternalError(OIDCException): + status = status.INTERNAL_SERVER_ERROR + friendly_message = "The system broke while trying to log you in." + + +class BannedUser(AuthenticationFailed): + friendly_message = "Your user is not valid in this application." diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py new file mode 100644 index 000000000..afcd7ee7e --- /dev/null +++ b/src/djangooidc/oidc.py @@ -0,0 +1,289 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import logging +import json + +from django.conf import settings +from django.http import HttpResponseRedirect +from Cryptodome.PublicKey.RSA import importKey +from jwkest.jwk import RSAKey +from oic import oic, rndstr +from oic.oauth2 import ErrorResponse +from oic.oic import AuthorizationRequest, AuthorizationResponse, RegistrationResponse +from oic.oic.message import AccessTokenResponse +from oic.utils.authn.client import CLIENT_AUTHN_METHOD +from oic.utils import keyio + +from . import exceptions as o_e + +__author__ = "roland" + +logger = logging.getLogger(__name__) + + +class Client(oic.Client): + def __init__(self, op): + """Step 1: Configure the OpenID Connect client.""" + logger.debug("Initializing the OpenID Connect client...") + try: + provider = settings.OIDC_PROVIDERS[op] + verify_ssl = getattr(settings, "OIDC_VERIFY_SSL", True) + except Exception as err: + logger.error(err) + logger.error("Configuration missing for OpenID Connect client") + raise o_e.InternalError() + + try: + # prepare private key for authentication method of private_key_jwt + key_bundle = keyio.KeyBundle() + rsa_key = importKey(provider["client_registration"]["sp_private_key"]) + key = RSAKey(key=rsa_key, use="sig") + key_bundle.append(key) + keyjar = keyio.KeyJar(verify_ssl=verify_ssl) + keyjar.add_kb("", key_bundle) + except Exception as err: + logger.error(err) + logger.error( + "Key jar preparation failed for %s", + provider["srv_discovery_url"], + ) + raise o_e.InternalError() + + try: + # create the oic client instance + super().__init__( + client_id=None, + client_authn_method=CLIENT_AUTHN_METHOD, + keyjar=keyjar, + verify_ssl=verify_ssl, + config=None, + ) + # must be set after client is initialized + self.behaviour = provider["behaviour"] + except Exception as err: + logger.error(err) + logger.error( + "Client creation failed for %s", + provider["srv_discovery_url"], + ) + raise o_e.InternalError() + + try: + # discover and store the provider (OP) urls, etc + self.provider_config(provider["srv_discovery_url"]) + self.store_registration_info( + RegistrationResponse(**provider["client_registration"]) + ) + except Exception as err: + logger.error(err) + logger.error( + "Provider info discovery failed for %s", + provider["srv_discovery_url"], + ) + raise o_e.InternalError() + + def create_authn_request( + self, + session, + extra_args=None, + ): + """Step 2: Construct a login URL at OP's domain and send the user to it.""" + logger.debug("Creating the OpenID Connect authn request...") + state = rndstr(size=32) + try: + session["state"] = state + session["nonce"] = rndstr(size=32) + scopes = list(self.behaviour.get("scope", [])) + scopes.append("openid") + request_args = { + "response_type": self.behaviour.get("response_type"), + "scope": " ".join(set(scopes)), + "state": session["state"], + "nonce": session["nonce"], + "redirect_uri": self.registration_response["redirect_uris"][0], + "acr_values": self.behaviour.get("acr_value"), + } + + if extra_args is not None: + request_args.update(extra_args) + except Exception as err: + logger.error(err) + logger.error("Failed to assemble request arguments for %s" % state) + raise o_e.InternalError(locator=state) + + logger.debug("request args: %s" % request_args) + + try: + # prepare the request for sending + cis = self.construct_AuthorizationRequest(request_args=request_args) + logger.debug("request: %s" % cis) + + # obtain the url and headers from the prepared request + url, body, headers, cis = self.uri_and_body( + AuthorizationRequest, + cis, + method="GET", + request_args=request_args, + ) + logger.debug("body: %s" % body) + logger.debug("URL: %s" % url) + logger.debug("headers: %s" % headers) + except Exception as err: + logger.error(err) + logger.error("Failed to prepare request for %s" % state) + raise o_e.InternalError(locator=state) + + try: + # create the redirect object + response = HttpResponseRedirect(str(url)) + # add headers to the object, if any + if headers: + for key, value in headers.items(): + response[key] = value + except Exception as err: + logger.error(err) + logger.error("Failed to create redirect object for %s" % state) + raise o_e.InternalError(locator=state) + + return response + + def callback(self, unparsed_response, session): + """Step 3: Receive OP's response, request an access token, and user info.""" + logger.debug("Processing the OpenID Connect callback response...") + state = session.get("state", "") + try: + # parse the response from OP + authn_response = self.parse_response( + AuthorizationResponse, + unparsed_response, + sformat="dict", + keyjar=self.keyjar, + ) + except Exception as err: + logger.error(err) + logger.error("Unable to parse response for %s" % state) + raise o_e.AuthenticationFailed(locator=state) + + # ErrorResponse is not raised, it is passed back... + if isinstance(authn_response, ErrorResponse): + error = authn_response.get("error", "") + if error == "login_required": + logger.warning( + "User was not logged in (%s), trying again for %s" % (error, state) + ) + return self.create_authn_request(session) + else: + logger.error("Unable to process response %s for %s" % (error, state)) + raise o_e.AuthenticationFailed(locator=state) + + logger.debug("authn_response %s" % authn_response) + + if not authn_response.get("state", None): + logger.error("State value not received from OP for %s" % state) + raise o_e.AuthenticationFailed(locator=state) + + if authn_response["state"] != session.get("state", None): + # this most likely means the user's Django session vanished + logger.error("Received state not the same as expected for %s" % state) + raise o_e.AuthenticationFailed(locator=state) + + if self.behaviour.get("response_type") == "code": + # need an access token to get user info (and to log the user out later) + self._request_token( + authn_response["state"], authn_response["code"], session + ) + + user_info = self._get_user_info(state, session) + + return user_info + + def _get_user_info(self, state, session): + """Get information from OP about the user.""" + scopes = list(self.behaviour.get("user_info_request", [])) + scopes.append("openid") + try: + # get info about the user from OP + info_response = self.do_user_info_request( + state=session["state"], + method="GET", + scope=" ".join(set(scopes)), + ) + except Exception as err: + logger.error(err) + logger.error("Unable to request user info for %s" % state) + raise o_e.AuthenticationFailed(locator=state) + + # ErrorResponse is not raised, it is passed back... + if isinstance(info_response, ErrorResponse): + logger.error( + "Unable to get user info (%s) for %s" + % (info_response.get("error", ""), state) + ) + raise o_e.AuthenticationFailed(locator=state) + + logger.debug("user info: %s" % info_response) + return info_response.to_dict() + + def _request_token(self, state, code, session): + """Request a token from OP to allow us to then request user info.""" + try: + token_response = self.do_access_token_request( + scope="openid", + state=state, + request_args={ + "code": code, + "redirect_uri": self.registration_response["redirect_uris"][0], + "client_id": self.client_id, + "client_secret": self.client_secret, + }, + authn_method=self.registration_response["token_endpoint_auth_method"], + ) + except Exception as err: + logger.error(err) + logger.error("Unable to obtain access token for %s" % state) + raise o_e.AuthenticationFailed(locator=state) + + # ErrorResponse is not raised, it is passed back... + if isinstance(token_response, ErrorResponse): + logger.error( + "Unable to get token (%s) for %s" + % (token_response.get("error", ""), state) + ) + raise o_e.AuthenticationFailed(locator=state) + + logger.debug("token response %s" % token_response) + + try: + # get the token and other bits of info + id_token = token_response["id_token"]._dict + + if id_token["nonce"] != session["nonce"]: + logger.error("Received nonce not the same as expected for %s" % state) + raise o_e.AuthenticationFailed(locator=state) + + session["id_token"] = id_token + session["id_token_raw"] = getattr(self, "id_token_raw", None) + session["access_token"] = token_response["access_token"] + session["refresh_token"] = token_response.get("refresh_token", "") + session["expires_in"] = token_response.get("expires_in", "") + self.id_token[state] = getattr(self, "id_token_raw", None) + except Exception as err: + logger.error(err) + logger.error("Unable to parse access token response for %s" % state) + raise o_e.AuthenticationFailed(locator=state) + + def store_response(self, resp, info): + """Make raw ID token available for internal use.""" + if isinstance(resp, AccessTokenResponse): + info = json.loads(info) + self.id_token_raw = info["id_token"] + + super(Client, self).store_response(resp, info) + + def __repr__(self): + return "Client {} {} {}".format( + self.client_id, + self.client_prefs, + self.behaviour, + ) diff --git a/src/djangooidc/urls.py b/src/djangooidc/urls.py new file mode 100644 index 000000000..ecd3a81a2 --- /dev/null +++ b/src/djangooidc/urls.py @@ -0,0 +1,12 @@ +# coding: utf-8 + +from django.urls import path + +from . import views + +urlpatterns = [ + path("login/", views.openid, name="openid"), + path("callback/login/", views.login_callback, name="openid_login_callback"), + path("logout/", views.logout, name="logout"), + path("callback/logout/", views.logout_callback, name="openid_logout_callback"), +] diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py new file mode 100644 index 000000000..eeadd4f31 --- /dev/null +++ b/src/djangooidc/views.py @@ -0,0 +1,120 @@ +# coding: utf-8 + +import logging + +from django.conf import settings +from django.contrib.auth import logout as auth_logout +from django.contrib.auth import authenticate, login +from django.http import HttpResponseRedirect +from django.shortcuts import redirect, render +from urllib.parse import parse_qs, urlencode + +from djangooidc.oidc import Client +from djangooidc import exceptions as o_e + + +logger = logging.getLogger(__name__) + +try: + # Initialize provider using pyOICD + OP = getattr(settings, "OIDC_ACTIVE_PROVIDER") + CLIENT = Client(OP) + logger.debug("client initialized %s" % CLIENT) +except Exception as err: + logger.warning(err) + logger.warning("Unable to configure OpenID Connect provider. Users cannot log in.") + + +def error_page(request, error): + """Display a sensible message and log the error.""" + logger.error(error) + if isinstance(error, o_e.AuthenticationFailed): + return render( + request, + "401.html", + context={ + "friendly_message": error.friendly_message, + "log_identifier": error.locator, + }, + status=401, + ) + if isinstance(error, o_e.InternalError): + return render( + request, + "500.html", + context={ + "friendly_message": error.friendly_message, + "log_identifier": error.locator, + }, + status=500, + ) + if isinstance(error, Exception): + return render(request, "500.html", status=500) + + +def openid(request): + """Redirect the user to an authentication provider (OP).""" + request.session["next"] = request.GET.get("next", "/") + + try: + return CLIENT.create_authn_request(request.session) + except Exception as err: + return error_page(request, err) + + +def login_callback(request): + """Analyze the token returned by the authentication provider (OP).""" + try: + query = parse_qs(request.GET.urlencode()) + userinfo = CLIENT.callback(query, request.session) + user = authenticate(request=request, **userinfo) + if user: + login(request, user) + logger.info("Successfully logged in user %s" % user) + return redirect(request.session.get("next", "/")) + else: + raise o_e.BannedUser() + except Exception as err: + return error_page(request, err) + + +def logout(request, next_page=None): + """Redirect the user to the authentication provider (OP) logout page.""" + try: + username = request.user.username + request_args = { + # it is perfectly fine to send the token, even if it is expired + "id_token_hint": request.session["id_token_raw"], + "state": request.session["state"], + } + if ( + "post_logout_redirect_uris" in CLIENT.registration_response.keys() + and len(CLIENT.registration_response["post_logout_redirect_uris"]) > 0 + ): + request_args.update( + { + "post_logout_redirect_uri": CLIENT.registration_response[ + "post_logout_redirect_uris" + ][0] + } + ) + + url = CLIENT.provider_info["end_session_endpoint"] + url += "?" + urlencode(request_args) + return HttpResponseRedirect(url) + except Exception as err: + return error_page(request, err) + finally: + # Always remove Django session stuff - even if not logged out from OP. + # Don't wait for the callback as it may never come. + auth_logout(request) + logger.info("Successfully logged out user %s" % username) + next_page = getattr(settings, "LOGOUT_REDIRECT_URL", None) + if next_page: + request.session["next"] = next_page + + +def logout_callback(request): + """Simple redirection view: after logout, redirect to `next`.""" + next = request.session["next"] if "next" in request.session.keys() else "/" + return redirect(next) diff --git a/src/docker-compose.yml b/src/docker-compose.yml index ba356d6a9..d104a4c15 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -25,6 +25,11 @@ services: - DJANGO_SECRET_KEY=feedabee # Run Django in debug mode on local - DJANGO_DEBUG=True + # Tell Django where it is being hosted + - DJANGO_BASE_URL="localhost:8080" + # --- These keys are obtained from `.env` file --- + # Set a private JWT signing key for Login.gov + - DJANGO_SECRET_LOGIN_KEY stdin_open: true tty: true ports: diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index e582da4ff..1e4b921d9 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -17,6 +17,7 @@ $ docker-compose exec app python manage.py shell """ import environs +from base64 import b64decode from cfenv import AppEnv from pathlib import Path @@ -43,7 +44,9 @@ path = Path(__file__) env_db_url = env.dj_db_url("DATABASE_URL") env_debug = env.bool("DJANGO_DEBUG", default=False) env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG") +env_base_url = env.str("DJANGO_BASE_URL") +secret_login_key = b64decode(secret("DJANGO_SECRET_LOGIN_KEY", "")) secret_key = secret("DJANGO_SECRET_KEY") # region: Basic Django Config-----------------------------------------------### @@ -78,6 +81,8 @@ INSTALLED_APPS = [ # (and any other places you specify) into a single location # that can easily be served in production "django.contrib.staticfiles", + # application used for integrating with Login.gov + "djangooidc", # let's be sure to install our own application! "registrar", ] @@ -268,12 +273,32 @@ USE_TZ = True # endregion # region: Logging-----------------------------------------------------------### -# No file logger is configured, because containerized apps -# do not log to the file system. -# TODO: Configure better logging options +# A Python logging configuration consists of four parts: +# Loggers +# Handlers +# Filters +# Formatters +# https://docs.djangoproject.com/en/4.1/topics/logging/ + +# Log a message by doing this: +# +# import logging +# logger = logging.getLogger(__name__) +# +# Then: +# +# logger.debug("We're about to execute function xyz. Wish us luck!") +# logger.info("Oh! Here's something you might want to know.") +# logger.warning("Something kinda bad happened.") +# logger.error("Can't do this important task. Something is very wrong.") +# logger.critical("Going to crash now.") + LOGGING = { "version": 1, - "disable_existing_loggers": False, + # Don't import Django's existing loggers + "disable_existing_loggers": True, + # define how to convert log messages into text; + # each handler has its choice of format "formatters": { "verbose": { "format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] " @@ -283,29 +308,62 @@ LOGGING = { "simple": { "format": "%(levelname)s %(message)s", }, + "django.server": { + "()": "django.utils.log.ServerFormatter", + "format": "[{server_time}] {message}", + "style": "{", + }, }, + # define where log messages will be sent; + # each logger can have one or more handlers "handlers": { "console": { - "level": "INFO", + "level": env_log_level, "class": "logging.StreamHandler", "formatter": "verbose", }, + "django.server": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "django.server", + }, + # No file logger is configured, + # because containerized apps + # do not log to the file system. }, + # define loggers: these are "sinks" into which + # messages are sent for processing "loggers": { + # Django's generic logger "django": { "handlers": ["console"], - "propagate": True, - "level": env_log_level, + "level": "INFO", }, + # Django's template processor "django.template": { "handlers": ["console"], - "propagate": True, "level": "INFO", }, + # Django's runserver + "django.server": { + "handlers": ["django.server"], + "level": "INFO", + "propagate": False, + }, + # OpenID Connect logger + "oic": { + "handlers": ["console"], + "level": "INFO", + }, + # Django wrapper for OpenID Connect + "djangooidc": { + "handlers": ["console"], + "level": "INFO", + }, + # Our app! "registrar": { "handlers": ["console"], - "propagate": True, - "level": "INFO", + "level": "DEBUG", }, }, } @@ -313,41 +371,49 @@ LOGGING = { # endregion # region: Login-------------------------------------------------------------### -# TODO: FAC example for login.gov -# SIMPLE_JWT = { -# "ALGORITHM": "RS256", -# "AUDIENCE": None, -# "ISSUER": "https://idp.int.identitysandbox.gov/", -# "JWK_URL": "https://idp.int.identitysandbox.gov/api/openid_connect/certs", -# "LEEWAY": 0, -# "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.UntypedToken",), -# "USER_ID_CLAIM": "sub", -# } -# TOKEN_AUTH = {"TOKEN_TTL": 3600} +# list of Python classes used when trying to authenticate a user +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "djangooidc.backends.OpenIdConnectBackend", +] -# endregion -# region: Rest Framework/API------------------------------------------------### +# this is where unauthenticated requests are redirected when using +# the login_required() decorator, LoginRequiredMixin, or AccessMixin +LOGIN_URL = "openid/openid/login" -# Enable CORS if api is served at subdomain -# https://github.com/adamchainz/django-cors-headers -# TODO: FAC example for REST framework -# API_VERSION = "0" -# REST_FRAMEWORK = { -# "DEFAULT_AUTHENTICATION_CLASSES": [ -# "rest_framework.authentication.BasicAuthentication", -# "users.auth.ExpiringTokenAuthentication", -# ], -# "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), -# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", -# "PAGE_SIZE": 10, -# "TEST_REQUEST_RENDERER_CLASSES": [ -# "rest_framework.renderers.MultiPartRenderer", -# "rest_framework.renderers.JSONRenderer", -# "rest_framework.renderers.TemplateHTMLRenderer", -# "rest_framework.renderers.BrowsableAPIRenderer", -# ], -# "TEST_REQUEST_DEFAULT_FORMAT": "api", -# } +# where to go after logging out +LOGOUT_REDIRECT_URL = "home" + +# disable dynamic client registration, +# only the OP inside OIDC_PROVIDERS will be available +OIDC_ALLOW_DYNAMIC_OP = False + +# which provider to use if multiple are available +# (code does not currently support user selection) +OIDC_ACTIVE_PROVIDER = "login.gov" + + +OIDC_PROVIDERS = { + "login.gov": { + "srv_discovery_url": "https://idp.int.identitysandbox.gov", + "behaviour": { + # the 'code' workflow requires direct connectivity from us to Login.gov + "response_type": "code", + "scope": ["email", "profile:name", "phone"], + "user_info_request": ["email", "first_name", "last_name", "phone"], + "acr_value": "http://idmanagement.gov/ns/assurance/ial/2", + }, + "client_registration": { + "client_id": "cisa_dotgov_registrar", + "redirect_uris": [f"https://{env_base_url}/openid/callback/login/"], + "post_logout_redirect_uris": [ + f"https://{env_base_url}/openid/callback/logout/" + ], + "token_endpoint_auth_method": ["private_key_jwt"], + "sp_private_key": secret_login_key, + }, + } +} # endregion # region: Routing-----------------------------------------------------------### diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 226e25beb..64b84dbff 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -10,10 +10,11 @@ from django.urls import include, path from registrar.views import health, index, profile urlpatterns = [ + path("", index.index, name="home"), path("admin/", admin.site.urls), - path("", index.index), path("health/", health.health), path("edit_profile/", profile.edit_profile, name="edit-profile"), + path("openid/", include("djangooidc.urls")), # these views respect the DEBUG setting path("__debug__/", include("debug_toolbar.urls")), ] diff --git a/src/registrar/templates/401.html b/src/registrar/templates/401.html new file mode 100644 index 000000000..23a48b8bd --- /dev/null +++ b/src/registrar/templates/401.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{% translate "Unauthorized" %}{% endblock %} + +{% block content %} +

{% translate "Unauthorized" %}

+ +{% if friendly_message %} +

{{ friendly_message }}

+{% else %} +

{% translate "Authorization failed." %}

+{% endif %} + +

+ {% translate "Would you like to try logging in again?" %} +

+ +{% if log_identifier %} +

Here's a unique identifier for this error.

+
{{ log_identifier }}
+

{% translate "Please include it if you contact us." %}

+{% endif %} + +TODO: Content team to create a "how to contact us" footer for the error pages + +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/404.html b/src/registrar/templates/404.html new file mode 100644 index 000000000..cac4df5d0 --- /dev/null +++ b/src/registrar/templates/404.html @@ -0,0 +1,13 @@ + +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{% translate "Page not found" %}{% endblock %} + +{% block content %} + +

{% translate "Page not found" %}

+ +

{% translate "The requested page could not be found." %}

+ +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/500.html b/src/registrar/templates/500.html new file mode 100644 index 000000000..9709d004f --- /dev/null +++ b/src/registrar/templates/500.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{% translate "Server error" %}{% endblock %} + +{% block content %} +

{% translate "Server Error" %}

+ +{% if friendly_message %} +

{{ friendly_message }}

+{% else %} +

{% translate "An internal server error occurred." %}

+{% endif %} + +{% if log_identifier %} +

Here's a unique identifier for this error.

+
{{ log_identifier }}
+

{% translate "Please include it if you contact us." %}

+{% endif %} + +TODO: Content team to create a "how to contact us" footer for the error pages + +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html new file mode 100644 index 000000000..14b0164ed --- /dev/null +++ b/src/registrar/templates/home.html @@ -0,0 +1,26 @@ + +{% extends 'base.html' %} + +{% block title %} Hello {% endblock %} +{% block hero %} +
+
+
+

+ Welcome to the .gov registrar +

+
+
+{% endblock %} + +{% block content %} +

This is the .gov registrar.

+ +{% if user.is_authenticated %} +

Hello {{ user.id }}

+

Click here to log out.

+{% else %} +

Click here to log in.

+{% endif %} + +{% endblock %}