diff --git a/.ml_style.rb b/.ml_style.rb index cb1eb4b0..6886eab3 100644 --- a/.ml_style.rb +++ b/.ml_style.rb @@ -3,5 +3,6 @@ # Enable all rules by default all -rule 'MD013', :line_length => 88 +rule 'MD013', :line_length => 88, :code_blocks => false rule 'MD007', :indent => 3 +rule 'MD024', :allow_different_nesting => true diff --git a/.travis.yml b/.travis.yml index ad941b3a..c0b81833 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,5 +29,6 @@ before_script: script: - black --check ./src - tox -c ./src/tox.ini -e coverage + - tox -c ./src/tox.ini -e oauth - docker-compose logs - docker ps | grep api | grep -q healthy diff --git a/README.md b/README.md index 0d628523..77916a38 100644 --- a/README.md +++ b/README.md @@ -345,116 +345,238 @@ specific to the sensor you are using. This section covers authentication, permissions, and certificates used to access the sensor, and the authentication available for the callback URL. Two different types of -authentication are available for authenticating against the sensor and for -authenticating when using a callback URL. **Note that the certificate authorities -(CAs), SSL certificates, private keys, and JWT public keys used in this repository are -for testing and development purposes only. They should not be used in a production -system.** +authentication are available for authenticating to the sensor and for +authenticating when using a callback URL. ### Sensor Authentication And Permissions -The sensor can be configured to authenticate using OAuth JWT access tokens from an -external authorization server or using Djnago Rest Framework Token Authentication. +The sensor can be configured to authenticate using OAuth 2 with an +external authorization server or using Django Rest Framework Token Authentication. #### Django Rest Framework Token Authentication This is the default authentication method. To enable Django Rest Framework -Authentication, make sure `AUTHENTICATION` is set to `TOKEN` in the environment file -(this will be enabled if `AUTHENTICATION` set to anything other -than `JWT`). +Authentication, make sure `AUTHENTICATION` is set to `TOKEN` in the environment file. +Token authentication will be used if `AUTHENTICATION` set to anything other +than `OAUTH`. A token is automatically created for each user. Django Rest Framework Token Authentication will check that the token in the Authorization header ("Token " + token) matches a user's token. -#### OAuth2 JWT Authentication +#### OAuth 2 Authentication + +To enable OAuth 2 Authentication, set `AUTHENTICATION` to `OAUTH` +in the environment file. To authenticate against the API, the client will first need to +get an access token from the authorization server. Then, in each request to the +sensor, the client sends the JWT access token in the authorization header (using +"Bearer " + access token). To authenticate against the browsable API, the OAuth 2 +authorization code flow is used. In the OAuth 2 authorization code flow, the user will +be redirected to the authorization server to enter their username and password before +being redirected back to the sensor. + +For the authorization code flow, set `CLIENT_ID` and `CLIENT_SECRET` in the environment +file. These parameters are used by the sensor to authenticate itself to the +authorization server. Also set `OAUTH_TOKEN_URL` and `OAUTH_AUTHORIZATION_URL` to the +token and authorization endpoints of the authroization server. + +The OAuth 2 access token (used both in the authorization header and authorization code +flow) signature will be verified using the public key from the `PATH_TO_JWT_PUBLIC_KEY` +setting. The access token verification checks additional properties including the access +token expiration time and client id. For scos-sensor, mutual TLS is required when using +OAuth 2 (see [Certificates](#Certificates) section below). As part of the OAuth 2 +access token verification, scos-sensor will verfiy the UID in the subject of the client +certificate matches the UID in the JWT access token. + +The token is expected to come from an OAuth 2 authorization server. For more +information, about OAuth 2 see . +[This section](https://tools.ietf.org/html/rfc6749#section-4.1) describes the +authorization code flow. + +Currently, only JWS (JSON Web Signature) JWTs are supported. -To enable OAuth 2 JWT Authentication, set `AUTHENTICATION` to `JWT` in the environment -file. To authenticate, the client will need to send a JWT access token in the -authorization header (using "Bearer " + access token). The token signature will be -verified using the public key from the `PATH_TO_JWT_PUBLIC_KEY` setting. The expiration -time will be checked. Only users who have an authority matching the `REQUIRED_ROLE` -setting will be authorized. +#### Certificates -The token is expected to come from an OAuth2 authorization server. For more -information, see . +This section describes how to create a self-signed root CA, SSL server certificates for +the sensor, optional client certificates, and test JWT public/private key pair. -#### Certificates +As described below, a self-signed CA can be created for testing. **For production, make +sure to use certificates from a trusted CA.** + +Below instructions adapted from +[here](https://www.golinuxcloud.com/openssl-create-client-server-certificate/#OpenSSL_create_client_certificate). + +##### Sensor Certificate + +This is the SSL certificate used for the scos-sensor web server and is always required. + +To be able to sign server-side and client-side certificates, we need to create our own +self-signed root CA certificate first. + +```bash +openssl req -x509 -sha512 -days 365 -newkey rsa:4096 -keyout scostestca.key -out scostestca.pem +``` + +Generate a host certificate signing request. + +```bash +openssl req -new -newkey rsa:4096 -keyout sensor01.key -out sensor01.csr -subj "/C=[2 letter country code]/ST=[state or province]/L=[locality]/O=[organization]/OU=[organizational unit]/UID=[user ID]/CN=[common name]" +``` + +Before we proceed with openssl, we need to create a configuration file -- sensor01.ext. +It'll store some additional parameters needed during signing the certificate. Adjust +the settings in the below example for your sensor: + +```text +authorityKeyIdentifier=keyid,issuer:always +basicConstraints=CA:FALSE +subjectAltName = @alt_names +subjectKeyIdentifier = hash +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth, clientAuth +[alt_names] +DNS.1 = sensor01.domain +DNS.2 = localhost +IP.1 = xxx.xxx.xxx.xxx +IP.2 = 127.0.0.1 +``` + +Sign the host certificate. + +```bash +openssl x509 -req -CA scostestca.pem -CAkey scostestca.key -in sensor01.csr -out sensor01.pem -days 365 -sha256 -CAcreateserial -extfile sensor01.ext +``` + +If the sensor private key is encrypted, decrypt it using the following command: + +```bash +openssl rsa -in sensor01.key -out sensor01_decrypted.key +``` + +Combine the sensor certificate and private key into one file: + +```bash +cat sensor01_decrypted.key sensor01.pem > sensor01_combined.pem +``` + +##### Client Certificate + +This certificate is required for using the sensor with mutual TLS which is required if +OAuth authentication is enabled. + +Replace the brackets with the information specific to your user and organization. + +```bash +openssl req -new -newkey rsa:4096 -keyout client.key -out client.csr -subj "/C=[2 letter country code]/ST=[state or province]/L=[locality]/O=[organization]/OU=[organizational unit]/UID=[user ID]/CN=[common name]" +``` -The NGINX web server requires an SSL certificate to use https. The certificate and -private key should be set using `SSL_CERT_PATH` and `SSL_KEY_PATH` in the environment -file. Note that these paths are relative to the configs/certs directory. - -Optionally, client certificates can be required. To require client certificates, -uncomment `ssl_verify_client on;` and `ssl_ocsp on;` in nginx/conf.template. Set the CA -certificate used for validating client certificates using the `SSL_CA_PATH` (relative -to configs/certs) in the environment file. - -##### Getting Certificates - -It is recommended to create your own CA for testing. **For production, make sure to use -certificates from a trusted CA.** For testing, you can use the certificates and keys in -configs/certs/test or you can use scripts/create_certificates.py to create the test -CA certificate, test server certificate, and test client certificate. This script can -also be used with an existing CA. Here are the instructions to use create_certificates -with an existing CA. - -1. To configure the create_certificates.py script, use create_certificates.ini. In - create_certificates.ini, set `ca_private_key_path` and `ca_certificate_path` to the - path of your CA private key and certificate. Configure the remaining parameters as - desired. The SAN (subject alternative name) parameters will need to be set to the - appropriate IP addresses and DNS names of your server and client. - -1. While in scos-sensor root directory, run the create_certificates.py script passing - the following arguments in the listed order: - - - ini_path - path to the create_certificates.ini file. - - ini_section - section of the INI file to use. - - key_passphrase - Passphrase to use to encrypt private keys. Set to `None` to - disable encryption. - - The following certificates will be generated: - - - sensor01_private.pem - sensor private key. - - sensor01_certificate.pem - sensor certificate. - - sensor01_client_private.pem - client private key. - - sensor01_client.pem - client certificate. - -1. Copy sensor01_private and sensor01_certificate to the computer where the scos-sensor - will run. If you are using client certificates, also copy the CA certificate used to - generate the certificates. Make sure the certificates are somewhere in configs/certs, - and that `SSL_CERT_PATH` and `SSL_KEY_PATH` (in the environment file) are set to the - paths of the certificates relative to configs/certs. If you are using client - certificates, set `SSL_CA_PATH` to the path of the CA certificate relative to - configs/certs. - -1. Run scos-sensor. If you are using client certificates, use - sensor01_client_private.pem and sensor01_client to connect to the API. - -The create_certificates.py script can also generate a new CA and use it for generating -the certificates. To run create_certificates.py this way, comment out -`ca_private_key_path` and `ca_certificate_path` in create_certificates.ini, make sure -`ca_private_key_save_path` and the other parameters are set as desired, then repeat -steps 2-4 above. The CA private key file (saved to ca_private_key_save_path) and the CA -public key (scostestca.crt) will be generated in addition to the files listed in step -2 above. +Create client.ext with the following: + +```text +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +keyUsage = digitalSignature +extendedKeyUsage = clientAuth +``` + +Sign the client certificate. + +```bash +openssl x509 -req -CA scostestca.pem -CAkey scostestca.key -in client.csr -out client.pem -days 365 -sha256 -CAcreateserial -extfile client.ext +``` + +Convert pem to pkcs12: + +```bash +openssl pkcs12 -export -out client.pfx -inkey client.key -in client.pem -certfile scostestca.pem +``` + +Import client.pfx into web browser. + +##### Generating JWT Public/Private Key + +The JWT public key must correspond to the private key of the JWT issuer (OAuth +authorization server). For manual testing, the instructions below could be used to +create a public/private key pair for creating JWTs without an authorization +server. + +###### Step 1: Create public/private key pair + +```bash +openssl genrsa -out jwt.pem 4096 +``` + +###### Step 2: Extract Public Key + +```bash +openssl rsa -in jwt.pem -outform PEM -pubout -out jwt_public_key.pem +``` + +###### Step 3: Extract Private Key + +```bash +openssl pkey -inform PEM -outform PEM -in jwt.pem -out jwt_private_key.pem +``` + +###### Configure scos-sensor + +The Nginx web server can be set to require client certificates (mutual TLS). This can +optionally be enabled for token authentication and is required for OAUTH +authentication. The client certificate subject UID is only verified for OAuth 2 +authentication, not for token authentication. To require client certificates, uncomment +`ssl_verify_client on;` in the [Nginx configuration file](nginx/conf.template). If you +use OCSP, also uncomment `ssl_ocsp on;`. Additional configuration may be needed for +Nginx to check certificate revocation lists (CRL). + +Copy the server certificate and server private key (sensor01_combined.pem) to +`scos-sensor/configs/certs`. Then set `SSL_CERT_PATH` and `SSL_KEY_PATH` (in the +environment file) to the path of the sensor01_combined.pem relative to configs/certs +(for file at `scos-sensor/configs/certs/sensor01_combined.pem`, set +`SSL_CERT_PATH=sensor01_combined.pem` and `SSL_KEY_PATH=sensor01_combined.pem`). For +mutual TLS/OAuth, also copy the CA certificate to the same directory. Then, set +`SSL_CA_PATH` to the path of the CA certificate relative to `configs/certs`. + +If you are using OAuth authentication, set `PATH_TO_JWT_PUBLIC_KEY` to the path of the +JWT public key relative to configs/certs. This public key file should correspond to the +private key of the OAuth authorization server. Alternatively, the JWT private key +created above could be used to manually sign a JWT token for testing if +`PATH_TO_JWT_PUBLIC_KEY` is set to the JWT public key created above. + +If you are using client certificates, use client.pfx to connect to the API by importing +this certificate into your browser. + +For callback functionality with an OAuth authorized callback URL, set +`PATH_TO_CLIENT_CERT` and `PATH_TO_VERIFY_CERT`, both relative to configs/certs. +Depending on the configuration of the callback URL server and the authorization server, +the sensor server certificate could be used as a client certificate by setting +`PATH_TO_CLIENT_CERT` to the path of sensor01_combined.pem relative to configs/certs. +Also the CA used to verify the client certificate could potentially be used to verify +the callback URL server certificate by setting `PATH_TO_VERIFY_CERT` to the same file +as used for `SSL_CA_PATH` (scostestca.pem). #### Permissions and Users -The API requires the user to either have an authority in the JWT token matching the the -`REQUIRED_ROLE` setting or that the user be a superuser. New users created using the +##### Token Permissions + +The API requires the user to be a superuser. New users created using the API initially do not have superuser access. However, an admin can mark a user as a -superuser in the Sensor Configuration Portal. When using JWT tokens, the user does not +superuser in the Sensor Configuration Portal. + +##### OAuth Permissions + +The API requires the user to either have an authority in the JWT token matching the the +`REQUIRED_ROLE` setting. When using JWT tokens, the user does not have to be pre-created using the sensor's API. The API will accept any user using a JWT token if they have an authority matching the required role setting. ### Callback URL Authentication -OAuth and Token authentication are supported for authenticating against the server +OAuth and Token authentication are supported for authenticating to the server pointed to by the callback URL. Callback SSL verification can be enabled or disabled using `CALLBACK_SSL_VERIFICATION` in the environment file. -#### Token +#### Token Callback Authentication A simple form of token authentication is supported for the callback URL. The sensor will send the user's (user who created the schedule) token in the authorization header @@ -463,30 +585,31 @@ the token against what it originally sent to the sensor when creating the schedu This method of authentication for the callback URL is enabled by default. To verify it is enabled, set `CALLBACK_AUTHENTICATION` to `TOKEN` in the environment file (this will be enabled if `CALLBACK_AUTHENTICATION` set to anything other than `OAUTH`). -`PATH_TO_VERIFY_CERT`, in the environment file, can used to set a CA certificate to +`PATH_TO_VERIFY_CERT`, in the environment file, can be used to set a CA certificate to verify the callback URL server SSL certificate. If this is unset and `CALLBACK_SSL_VERIFICATION` is set to true, [standard trusted CAs]( https://requests.readthedocs.io/en/master/user/advanced/#ca-certificates) will be -used. +used. If `CALLBACK_SSL_VERIFICATION` is not true, verification of the callback URL +server SSL certificate will not be performed. -#### OAuth +#### OAuth Callback Authentication The OAuth 2 password flow is supported for callback URL authentication. The following settings in the environment file are used to configure the OAuth 2 password flow authentication. - `CALLBACK_AUTHENTICATION` - set to `OAUTH`. -- `CLIENT_ID` - client ID used to authorize the client (the sensor) against the +- `CLIENT_ID` - client ID used to authorize the client (the sensor) to the authorization server. -- `CLIENT_SECRET` - client secret used to authorize the client (the sensor) against the +- `CLIENT_SECRET` - client secret used to authorize the client (the sensor) to the authorization server. - `OAUTH_TOKEN_URL` - URL to get the access token. -- `PATH_TO_CLIENT_CERT` - client certificate used to authenticate against the +- `PATH_TO_CLIENT_CERT` - client certificate used to authenticate to the authorization server. - `PATH_TO_VERIFY_CERT` - CA certificate to verify the authorization server and callback URL server SSL certificate. If this is unset and `CALLBACK_SSL_VERIFICATION` is set to true, [standard trusted CAs]( - https://requests.readthedocs.io/en/master/user/advanced/#ca-certificates) will be + https://requests.readthedocs.io/en/master/user/advanced/#ca-certificates) will be used. In src/sensor/settings.py, the OAuth `USER_NAME` and `PASSWORD` are set to be the same diff --git a/configs/certs/README.md b/configs/certs/README.md new file mode 100644 index 00000000..6a96bf2f --- /dev/null +++ b/configs/certs/README.md @@ -0,0 +1,3 @@ +# Certs + +Add SSL certificates and JWT public key here. diff --git a/configs/certs/test/jwt_pubkey.pem b/configs/certs/test/jwt_pubkey.pem deleted file mode 100644 index 321fdbf5..00000000 --- a/configs/certs/test/jwt_pubkey.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsN/pGkRlf8Z6sX/SCphV -5qZ6rrHxsY8H9QnZwO7gHYe1+wrSvKbVKtKvdNwQQzfZ9PNnfroZXXvLfZQysv+E -obrj69XaWnXnrltsNjLWmi3X+JXOzY5ZZKmSR748gDEHL74UdBjdU0lm3s/olfLW -LZZ0PeR+9BFgc3AFhIGZ5kLH/pSzTDhypbJx7gfRkkno7igeLHx7pqo/QEclYlhc -/fMjNN0rOqEGu4uroVUjsSUNvV4cLSB0Qc0NsSgQ70og885laoYTaUf5yYnqkXD1 -FTBXK0AeYeEb9ZUrkUXv5fVCZ1GS5O58IbowZ4ZREWoKo7GcEjAcb7duYOZROQKE -6ocmZXv3e6XJcXJG/7db5cREwwReCtFydg1HBL03JtJpWgpfroLGeS7XbNuZii7p -u/GW2htJ5jtu81SbRUsKPpETHa4hhdGoCVvFF8Vs6MYNo1yb/GfxRGmIwaDHlWss -8wFSKF4jUUAkPSY+dAzOPp/45RidLW8GaT1K/CM3z6ArLU0IeOSeGsF7tQQqNB9B -GcjPp6tUXgM0sCqeMAci+YhKAtBomxZCSySXL+VsXr1HzA6UeJbTUXNsdJOZv04V -346LPZc6MZAXQZS2vJwjHgwQfXOf9pgDoJmSVetfsUYx9+efh0nlNKxNev7Xk+za -0WVeMxW68C6sNsFHX4hagYcCAwEAAQ== ------END PUBLIC KEY----- diff --git a/configs/certs/test/scos_test_ca.crt b/configs/certs/test/scos_test_ca.crt deleted file mode 100644 index 88ff4cd9..00000000 --- a/configs/certs/test/scos_test_ca.crt +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFjzCCA3egAwIBAgIEEdWUoDANBgkqhkiG9w0BAQ0FADBjMQswCQYDVQQGEwJD -TzERMA8GA1UECBMIQ29sb3JhZG8xEDAOBgNVBAcTB0JvdWxkZXIxDTALBgNVBAoT -BE5USUExDDAKBgNVBAsTA0lUUzESMBAGA1UEAxMJU0NPUyBURVNUMB4XDTIwMDUy -OTE5NTEyN1oXDTMwMDUyNzE5NTEyN1owYzELMAkGA1UEBhMCQ08xETAPBgNVBAgT -CENvbG9yYWRvMRAwDgYDVQQHEwdCb3VsZGVyMQ0wCwYDVQQKEwROVElBMQwwCgYD -VQQLEwNJVFMxEjAQBgNVBAMTCVNDT1MgVEVTVDCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAL8NIZi1GIiNl0AiIzxzdnoBK/4MrVOyGP+v8U4mefMdk9yE -0FzbMg2SipOyuyP6oThZBNc7guDf4f0gWfnEQgpVrfiVEktpST3PEIR2hYJq2x6Y -TUwRCeZznJBiLEuqmMOvwyT7IN5icfI/bv5SjoWfWEG+oUhttGl/5urmCEc2CavI -p3GGbGqVHVDZ5Ub/CkIsAetWIlROdZx9QC7j5K+bZrDkINO3i7FL8X6YUVx+fWUc -Nji6KheqSloDO9nxPu5an/2GaaUXfakYR4uHJd/PtoQbYS3npYknY2mJb5U1gm7r -mECG6SPXn9/UrR/fBjoVrUY/4dA/wGRQMZ13v/jFjUjt3v5qQK+rcPJUbJ7xlJt5 -GhLFBJtqdP3Zaca2FaY31PmSXQ6nq26JRg4i5D3BJPq8wKzu994hAQ/tGlTS04fo -LrFEc0QBOPTNEjxYyaqjv4pyLSb17luXM7sbdZScWhVtmyi8aMngdDyVgSGRzRLe -+c1nlUM0ZZLvcSN34nVe9sry+nrRFR+vqmzrUN1JImABfYUaxBWUEd4MGowZ0fKj -jnmGhRUxf/+GU15bKUwQtS61+ltdwzxMvdbQMaKIwrlHY6AElB2a55wFQ72JKSwN -1Nkt0iuU5ckUqqDZvB44Qq9EAkigsBthOwqf3O2hVpsuAY+aR2fN4qrtCRh3AgMB -AAGjSzBJMB0GA1UdDgQWBBSi5vMLp72hgZIDDt4KEDnR9ZKXmjAaBgNVHREEEzAR -gglsb2NhbGhvc3SHBH8AAAEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOC -AgEAFM4BVoObSJJ1+gLC1+3q/zgHbKHfTiK9eibxqrJVKHf4KJJPgvken8ld2BtW -zSEwQOhRD6/XkE31b3HCMHjsvwF6l7jr+qsnZY8/Sfj6KG+G9cPPYPOIo3z+iCXD -flVvpPwJtOfWV8gW5HAWTTRt6oN7MQ7hJjbyn51/TH8CQ+6C5cM0DeuxUuWfw0X1 -lffzrxCWEkzVXmwH3oUl2V640fGJfwB7nOkhO2Xnulrfj8s7w1gW24z9OM7SM2Y6 -i88i8YcbD93QvsRtSMunajtkaQJahkicfyA/Ar3Vh/7PzRP7SU+v5J+2tzIR2ds1 -5/XGhabUe/RX//+BV6yxEDgTW0j5BEphiKi1k/Mjc/KFyirdibU9WskAWFz8FnnL -MQh/Vnvlz8qrp4tEdr1b8tu1KqeQ0bak0riiQ9FkSBv4m0q+EZCe7iBs7zH8J1QS -AyQ7y3W0tyt3bYMcIO+Qx0CAXNjOH+3kCVm9a+877uYizIgRY1IqeF2x0T+C3uRM -puBBL6nEyFatvs/kj9Ujggd8Cd6lecv4j9fJRS4UBtLlSORuqHXqM8kg2fAAmlRT -DAo+kqJsr91kjYJve4Lt+4cYZ0fYaf/UzQTWnKX4NBpyA0ak/+7GdO9+1JsIpud+ -R7Qb68btglYI6v9UPI493X3bMh7yz62uhLge2tdA8GV67Jk= ------END CERTIFICATE----- diff --git a/configs/certs/test/sensor01.pem b/configs/certs/test/sensor01.pem deleted file mode 100644 index 6e81df1f..00000000 --- a/configs/certs/test/sensor01.pem +++ /dev/null @@ -1,131 +0,0 @@ -Bag Attributes - localKeyID: 98 32 E7 32 D4 0D A7 4A 2B EC 90 45 1D 1B C6 87 3C E0 58 19 -subject=C = CO, ST = Colorado, L = Boulder, O = NTIA, OU = ITS, CN = sensor01 - -issuer=C = CO, ST = Colorado, L = Boulder, O = NTIA, OU = ITS, CN = SCOS TEST - ------BEGIN CERTIFICATE----- -MIIFsTCCA5mgAwIBAgIUTKHkxQjciPZR/HrkptDMFnOSYSIwDQYJKoZIhvcNAQEN -BQAwYzELMAkGA1UEBhMCQ08xETAPBgNVBAgTCENvbG9yYWRvMRAwDgYDVQQHEwdC -b3VsZGVyMQ0wCwYDVQQKEwROVElBMQwwCgYDVQQLEwNJVFMxEjAQBgNVBAMTCVND -T1MgVEVTVDAeFw0yMDExMDkxOTUxMjlaFw0yMTExMDkxOTUxMjlaMGIxCzAJBgNV -BAYTAkNPMREwDwYDVQQIDAhDb2xvcmFkbzEQMA4GA1UEBwwHQm91bGRlcjENMAsG -A1UECgwETlRJQTEMMAoGA1UECwwDSVRTMREwDwYDVQQDDAhzZW5zb3IwMTCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALJdi8bpf9H0jP4TUWg07vxutc+H -hjXbtWvy3V8C53QH0XMse4p0vRgHHvwJ0pvR5Ef0d06BoKC2aQQ31VNMqaEdI4w5 -4VzzflWE3EmSKLnsfZIgBUsfx7UxnBT5R1UxAxJRLRj3zxJWVnVVK3YZ9XKx+YyY -F4rkyCZCBA8Ol8rOu2uVlIJr3532vKZ1bn6cqFiRUcgpMH1zMycYPX/UcB/l3rEh -cH6Wvw2Gx00hOzpF7lJUg4Wva1g1CCVdnRIP1WNPngpRxDdbCPWADS8+6uKETnW5 -fMKXYMBz56zp2aWcuatr/InqYsptAY3YOS2C39XPaaecvxU43brcWpoNERcsvRgA -VSyc0uww9U4u3rRox8KcPc64wjwkBtba2egFUERY8an5UI5IxfwaEdFKnVodMACD -4D4XSL/h3PekAK7Ay55RX96h666v+1ii0gKv/XMCbvoSKnSPvD/fB5R4xGrpv7d1 -/Btyp35It/qHTfRh3FgUsdwh1LU+pEt+yXkpI+zVhJ6euuezQny7sJA/ODfEvqER -5l1/P05B0s7Y0xgb/Z+v6l/4MNu0p2xuHkQOTZAns8K7iYgQgBmZttllgYYrVn/N -VGjqnQVuQ2krquXhoxMP4PFHpQz0Jb9w5I1oGm2+j7Wx5qlNQ9JNK8vS10VhaJXi -YLjjFYyxSBnH8JlXAgMBAAGjXjBcMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAA -ATAdBgNVHQ4EFgQU7DVbVmyC7+NQfGEWxJLIIrBCznAwHwYDVR0jBBgwFoAUoubz -C6e9oYGSAw7eChA50fWSl5owDQYJKoZIhvcNAQENBQADggIBABX9PKEhqpXcqXT5 -midLxzzRXjK6JXR72ZKcOb7k2y9JprLS2vLSVO5lywjCv12CJ4OmaXOrQ0VSbvH9 -75EXn6KHx1bM8nrBZyeKik33AaWNRxrRm0wUot47+uHLX2xGWK1tAckuV4Ynku4M -AfQ683FnyTKupmm/PAulx6R5dT47SOXJK6C5CT1fQeq/svErl74jFGqaMRoeowwR -g1t+//sbldgt4bEYiDKFPNLx9PgZyDdE8bM1FzJatRduNzDE0XqOKimoH3/4SyDJ -L7QeU7QZJoF9ckN21cgKeH5c3Nl3/5VrwTC5G1OvCHsQi4/Mq9xNZdRmzJNkECwg -480glqh417LkavtlMOoB9g9WSkAut+bkhW1c+YQjN/hdph6d/73y5xS0s7j2RiCH -pNcd9oFT6diBAxY01tLjh2dtgrWT9ZPB7F67cIV+2CWYtHpbSHvaylvsQs76NW+1 -mlnZddaiL90h95qkfqwjRZJSJSJ3oyDE0jHzkebZChXxZGM8Vx+2PgL+QV9FBY0A -U+GMrx/30MtwQkJK4ZoXOsys5Mbryc/ANsK7AA1zxlZBoVPL2Ayam7qehcLPmdaZ -kzElONTmptpSnYLoa6IRCo+jPl3pUFIL9iIwTTgO+irS4TJdv1pKVdvA4BmwPxWu -R/qhi4W5tKKq8DacqOWPct/YA6Rl ------END CERTIFICATE----- -Bag Attributes: -subject=C = CO, ST = Colorado, L = Boulder, O = NTIA, OU = ITS, CN = SCOS TEST - -issuer=C = CO, ST = Colorado, L = Boulder, O = NTIA, OU = ITS, CN = SCOS TEST - ------BEGIN CERTIFICATE----- -MIIFjzCCA3egAwIBAgIEEdWUoDANBgkqhkiG9w0BAQ0FADBjMQswCQYDVQQGEwJD -TzERMA8GA1UECBMIQ29sb3JhZG8xEDAOBgNVBAcTB0JvdWxkZXIxDTALBgNVBAoT -BE5USUExDDAKBgNVBAsTA0lUUzESMBAGA1UEAxMJU0NPUyBURVNUMB4XDTIwMDUy -OTE5NTEyN1oXDTMwMDUyNzE5NTEyN1owYzELMAkGA1UEBhMCQ08xETAPBgNVBAgT -CENvbG9yYWRvMRAwDgYDVQQHEwdCb3VsZGVyMQ0wCwYDVQQKEwROVElBMQwwCgYD -VQQLEwNJVFMxEjAQBgNVBAMTCVNDT1MgVEVTVDCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAL8NIZi1GIiNl0AiIzxzdnoBK/4MrVOyGP+v8U4mefMdk9yE -0FzbMg2SipOyuyP6oThZBNc7guDf4f0gWfnEQgpVrfiVEktpST3PEIR2hYJq2x6Y -TUwRCeZznJBiLEuqmMOvwyT7IN5icfI/bv5SjoWfWEG+oUhttGl/5urmCEc2CavI -p3GGbGqVHVDZ5Ub/CkIsAetWIlROdZx9QC7j5K+bZrDkINO3i7FL8X6YUVx+fWUc -Nji6KheqSloDO9nxPu5an/2GaaUXfakYR4uHJd/PtoQbYS3npYknY2mJb5U1gm7r -mECG6SPXn9/UrR/fBjoVrUY/4dA/wGRQMZ13v/jFjUjt3v5qQK+rcPJUbJ7xlJt5 -GhLFBJtqdP3Zaca2FaY31PmSXQ6nq26JRg4i5D3BJPq8wKzu994hAQ/tGlTS04fo -LrFEc0QBOPTNEjxYyaqjv4pyLSb17luXM7sbdZScWhVtmyi8aMngdDyVgSGRzRLe -+c1nlUM0ZZLvcSN34nVe9sry+nrRFR+vqmzrUN1JImABfYUaxBWUEd4MGowZ0fKj -jnmGhRUxf/+GU15bKUwQtS61+ltdwzxMvdbQMaKIwrlHY6AElB2a55wFQ72JKSwN -1Nkt0iuU5ckUqqDZvB44Qq9EAkigsBthOwqf3O2hVpsuAY+aR2fN4qrtCRh3AgMB -AAGjSzBJMB0GA1UdDgQWBBSi5vMLp72hgZIDDt4KEDnR9ZKXmjAaBgNVHREEEzAR -gglsb2NhbGhvc3SHBH8AAAEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOC -AgEAFM4BVoObSJJ1+gLC1+3q/zgHbKHfTiK9eibxqrJVKHf4KJJPgvken8ld2BtW -zSEwQOhRD6/XkE31b3HCMHjsvwF6l7jr+qsnZY8/Sfj6KG+G9cPPYPOIo3z+iCXD -flVvpPwJtOfWV8gW5HAWTTRt6oN7MQ7hJjbyn51/TH8CQ+6C5cM0DeuxUuWfw0X1 -lffzrxCWEkzVXmwH3oUl2V640fGJfwB7nOkhO2Xnulrfj8s7w1gW24z9OM7SM2Y6 -i88i8YcbD93QvsRtSMunajtkaQJahkicfyA/Ar3Vh/7PzRP7SU+v5J+2tzIR2ds1 -5/XGhabUe/RX//+BV6yxEDgTW0j5BEphiKi1k/Mjc/KFyirdibU9WskAWFz8FnnL -MQh/Vnvlz8qrp4tEdr1b8tu1KqeQ0bak0riiQ9FkSBv4m0q+EZCe7iBs7zH8J1QS -AyQ7y3W0tyt3bYMcIO+Qx0CAXNjOH+3kCVm9a+877uYizIgRY1IqeF2x0T+C3uRM -puBBL6nEyFatvs/kj9Ujggd8Cd6lecv4j9fJRS4UBtLlSORuqHXqM8kg2fAAmlRT -DAo+kqJsr91kjYJve4Lt+4cYZ0fYaf/UzQTWnKX4NBpyA0ak/+7GdO9+1JsIpud+ -R7Qb68btglYI6v9UPI493X3bMh7yz62uhLge2tdA8GV67Jk= ------END CERTIFICATE----- -Bag Attributes - localKeyID: 98 32 E7 32 D4 0D A7 4A 2B EC 90 45 1D 1B C6 87 3C E0 58 19 -Key Attributes: ------BEGIN PRIVATE KEY----- -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 -3qV65+xgHerG5pMxhdv30beZuGXM0Q== ------END PRIVATE KEY----- diff --git a/configs/certs/test/test_bad_private_key.pem b/configs/certs/test/test_bad_private_key.pem deleted file mode 100644 index 98570627..00000000 --- a/configs/certs/test/test_bad_private_key.pem +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- diff --git a/configs/certs/test/test_bad_pubkey.pem b/configs/certs/test/test_bad_pubkey.pem deleted file mode 100644 index 33edde75..00000000 --- a/configs/certs/test/test_bad_pubkey.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp6cYC+PB3aDwVUDYM+LC -C6klPUqFEho6rH7SzG0kkqUYCtXJFsnqDgsA/Q74gUgAlx7x8pGU3ulFRBgSYYN0 -I8Hw1rpLz2X0rJ46PH5feYw33TGUQtVYd+nhD/8wBSWyo9FQ8MTD+WbGUXRoT4D3 -Eh6YbOETPYch0JLlxAj/2TlKgQKgfLze4J4ofa+AZ7XK8BqnhXLpWkK5qwRY7kKc -M/CpUaPJDDp3pxgGYSvRh5IYD4wbiib0u71hLXCOCftWjFGXpw9gMgX5E6lzjXBn -ymgR/QIxe0RElH7Q/EcjwZNLK0e3JrTqBOITJqErclcKTB2vW021SdXeuXp9YoMi -Ekt0sv8nM4QYbXssPI3D0RJmKpehI1xIjZIxMWQXeNSQ65fx70LbXUyjuOJGyJsk -JtVsTdc8Iu7QBFYcABbTi097FwQTj7kXvI3A0GUTfee/O5OJ4nk/BYf20A31KoAI -DNNLCyWLvHyUZVyJ1j/Gw8JcyY9nh98YzKQjTNRk3y7pF5vaNk7CTMn/xAll5Kyg -hMAS/xZVoJbcsln5CWd9TUoYBuoNXFDk9v0PPKz0E1TLQ5HYp3dwiF+wtlIUR8JA -yh186gvpXLiLltx5KaBbgS3Y+vT3QXtrgbyE0wSY3GheqO/NvzG5HCGOlpmGjahS -7TAO60fXrXoovjG7GMK/b+0CAwEAAQ== ------END PUBLIC KEY----- diff --git a/configs/certs/test/test_private_key.pem b/configs/certs/test/test_private_key.pem deleted file mode 100644 index 90830515..00000000 --- a/configs/certs/test/test_private_key.pem +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- diff --git a/configs/certs/test/test_pubkey.pem b/configs/certs/test/test_pubkey.pem deleted file mode 100644 index b00dc1f4..00000000 --- a/configs/certs/test/test_pubkey.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2GFhjkqMXlFzJeaaeS4u -pekdnX5ZWlTB4gf0x1f5VqpEdpwaM3fWGULNH0z1aM2IMz7LqJem8juo5Pf332nc -dAytyRPZxGgdNui2adVW80NpNjyMM4C2yDhF5++YTQsGkRi4WbG9759JVKiNOiog -mgzfyohqGSlKnBvl4c0R944iIzQWHTzinyAvInwv/QdbI7Ri0G/u0KZhhxeSVPWR -8DPtFz9InBCB3Kn7lZtPD8k7rh+r35mBUL1DVCtqOlaC96vmWxdKbv5YZ4rd2k65 -rTrnkcqhttAuvt8ljQVj32Lk+NVowbjElwAAvwW/U+CMXV6hByPPZ/TcJBKk9Ueu -ng6d9NqdTkrvZ65TH0bZb/rxsDbckAi1pVqEDSiQlhH83REdfscF2TpL4gvGgDSl -in997y6HIYf/jP5P2zUCDLkfvGzHBlE2NFsRrLx3uriEQr0xERpsEG3QLUvcY72E -TruVs50jmkjSjus6VL9RCafBAkBgUlknckmCxkzBLVi1mzGSOpgEwKfo7kqqkmbi -05dPC+Lq88w8KVd68Teq91JCFBjepc9lK0Oxm9Jc/OR2YLPV2Wg+1WaYazy2Nd0O -C1BmClaCe9YKgJEv6a8oJokQwC72friEUGD0/fxfKh+tGiXl0mJSGMog+uOb/F82 -1E0EbtVO86Pl+RCCImtvcxECAwEAAQ== ------END PUBLIC KEY----- diff --git a/docker-compose.yml b/docker-compose.yml index ac7d3709..b7c9827f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,8 @@ services: - MAX_DISK_USAGE - MOCK_RADIO - MOCK_RADIO_RANDOM + - MAX_TASK_RESULTS + - OAUTH_AUTHORIZATION_URL - OAUTH_TOKEN_URL - PATH_TO_CLIENT_CERT - PATH_TO_JWT_PUBLIC_KEY diff --git a/docker/Dockerfile-nginx b/docker/Dockerfile-nginx index d287f430..096cd7b5 100644 --- a/docker/Dockerfile-nginx +++ b/docker/Dockerfile-nginx @@ -1,4 +1,4 @@ -FROM nginx:1.17-alpine +FROM nginx:1.19-alpine RUN mkdir -p /var/www/scos-sensor/static COPY --chown=nginx:nginx ./src/static/ /var/www/scos-sensor/static/ diff --git a/env.template b/env.template index 9953d5f5..9c831463 100644 --- a/env.template +++ b/env.template @@ -24,9 +24,9 @@ FQDN="$(hostname -f)" # Provide the absolute path to your ssl certificate and key # Paths relative to configs/certs REPO_ROOT=$(git rev-parse --show-toplevel) -SSL_CERT_PATH=test/sensor01.pem -SSL_KEY_PATH=test/sensor01.pem -SSL_CA_PATH=test/scos_test_ca.crt +SSL_CERT_PATH=sensor01_combined.pem +SSL_KEY_PATH=sensor01_combined.pem +SSL_CA_PATH=scostestca.pem # Use latest as default for local development DOCKER_TAG=latest GIT_BRANCH="git:$(git rev-parse --abbrev-ref HEAD)@$(git rev-parse --short HEAD)" @@ -61,15 +61,16 @@ BASE_IMAGE=smsntia/uhd_b2xx_py3 # Set to OAUTH if using OAuth Password Flow Authentication, callback url needs to be api/v2/results CALLBACK_AUTHENTICATION=TOKEN -CLIENT_ID=sensor01.sms.internal +CLIENT_ID=sensor01.sms.internal # must match FQDN CLIENT_SECRET=sensor-secret OAUTH_TOKEN_URL=https://scosmgrqa01.sms.internal:443/authserver/oauth/token +OAUTH_AUTHORIZATION_URL=https://scosmgrqa01.sms.internal:443/authserver/oauth/authorize # Sensor certificate with private key used as client cert -PATH_TO_CLIENT_CERT=test/sensor01.pem +PATH_TO_CLIENT_CERT=sensor01_combined.pem # Trusted Certificate Authority certificate to verify authserver and callback URL server certificate -PATH_TO_VERIFY_CERT=test/scos_test_ca.crt +PATH_TO_VERIFY_CERT=scostestca.pem # Path relative to configs/certs -PATH_TO_JWT_PUBLIC_KEY=test/jwt_pubkey.pem -# set to JWT to enable JWT authentication +PATH_TO_JWT_PUBLIC_KEY=jwt_public_key.pem +# set to OAUTH to enable OAUTH authentication AUTHENTICATION=TOKEN diff --git a/nginx/conf.template b/nginx/conf.template index 30a4750a..3e531126 100644 --- a/nginx/conf.template +++ b/nginx/conf.template @@ -23,14 +23,22 @@ server { listen [::]:443 ssl; server_name ${DOMAINS}; + # https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/ + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + # reduce "upstream response is buffered to a temporary file" warnings proxy_buffers 16 16k; proxy_buffer_size 16k; + # https://www.ssl.com/guide/tls-standards-compliance/#appendix-example-tls-configurations + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_tickets off; ssl_certificate /etc/ssl/certs/ssl-cert.pem; ssl_certificate_key /etc/ssl/private/ssl-cert.key; - ssl_protocols TLSv1.2; + ssl_client_certificate /etc/ssl/certs/ca.crt; # ssl_verify_client on; # ssl_ocsp on; # Enable OCSP validation @@ -50,8 +58,13 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; proxy_redirect off; + proxy_set_header X-SSL-CLIENT-DN $ssl_client_s_dn; proxy_pass http://wsgi-server; + # https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/ + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; } } diff --git a/src/.isort.cfg b/src/.isort.cfg index 9d825df1..f406e2d2 100644 --- a/src/.isort.cfg +++ b/src/.isort.cfg @@ -4,4 +4,4 @@ include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 -known_third_party=cryptography,django,drf_yasg,environs,jsonfield,jwt,numpy,oauthlib,pem,pytest,requests_futures,requests_mock,requests_oauthlib,rest_framework,scos_actions,sigmf +known_third_party=cryptography,django,drf_yasg,environs,jsonfield,jwt,numpy,oauthlib,pytest,requests_futures,requests_mock,requests_oauthlib,rest_framework,scos_actions,sigmf diff --git a/src/actions/__init__.py b/src/actions/__init__.py index 92df5da7..32ac06a0 100644 --- a/src/actions/__init__.py +++ b/src/actions/__init__.py @@ -19,9 +19,7 @@ logger.debug(discovered_plugins) # Actions initialized here are made available through the API -registered_actions = { - "logger": logger_action.Logger(), -} +registered_actions = {"logger": logger_action.Logger()} for name, module in discovered_plugins.items(): diff --git a/src/authentication/auth.py b/src/authentication/auth.py index a8a94069..f1f59329 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -1,4 +1,5 @@ import logging +import re import jwt from django.conf import settings @@ -14,7 +15,12 @@ in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) oauth_jwt_authentication_enabled = ( - "authentication.auth.OAuthJWTAuthentication" + "authentication.auth.OAuthAPIJWTAuthentication" + in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] +) + +oauth_session_authentication_enabled = ( + "authentication.auth.OAuthSessionAuthentication" in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) @@ -28,7 +34,82 @@ def jwt_request_has_required_role(request): return False -class OAuthJWTAuthentication(authentication.BaseAuthentication): +def get_uid_from_dn(cert_dn): + p = re.compile("UID=(.*?)(?:,|\+|$)") + match = p.search(cert_dn) + if not match: + raise Exception("No UID found in certificate!") + uid_raw = match.group() + # logger.debug(f"uid_raw = {uid_raw}") + uid = uid_raw.split("=")[1].rstrip(",").rstrip("+") + # logger.debug(f"uid = {uid}") + return uid + + +def validate_token(token, cert_uid): + public_key = "" + try: + with open(settings.PATH_TO_JWT_PUBLIC_KEY) as public_key_file: + public_key = public_key_file.read() + except Exception as e: + logger.error(e) + if not public_key: + error = exceptions.AuthenticationFailed( + "Unable to get public key to decode jwt" + ) + logger.error(error) + raise error + try: + # decode JWT token + # verifies jwt signature using RS256 algorithm and public key + # requires exp claim to verify token is not expired + # decodes and returns base64 encoded payload + decoded_token = jwt.decode( + token, + public_key, + verify=True, + algorithms="RS256", + options={"require": ["exp"], "verify_exp": True}, + ) + if decoded_token["userDetails"]["uid"] != cert_uid: + # https://tools.ietf.org/id/draft-ietf-oauth-mtls-07.html#rfc.section.3 + token_uid = decoded_token["userDetails"]["uid"] + logger.debug(f"token uid {token_uid} does not match cert uid {cert_uid}") + raise Exception("JWT DN does not match client certificate DN!") + return decoded_token + except ExpiredSignatureError as e: + logger.error(e) + raise exceptions.AuthenticationFailed("Token is expired!") + except InvalidSignatureError as e: + logger.error(e) + raise exceptions.AuthenticationFailed("Unable to verify token!") + except Exception as e: + logger.error(e) + raise exceptions.AuthenticationFailed(f"Unable to decode token! {e}") + + +def get_or_create_user_from_token(decoded_token): + jwt_username = decoded_token["user_name"] + user_model = get_user_model() + user = None + try: + user = user_model.objects.get(username=jwt_username) + except user_model.DoesNotExist: + user = user_model.objects.create_user(username=jwt_username) + user.email = decoded_token["userDetails"]["email"] + user.save() + if decoded_token["authorities"]: + authorities = decoded_token["authorities"] + if settings.REQUIRED_ROLE.upper() in authorities: + user.is_staff = True + # user.is_superuser = True + else: + user.is_staff = False + user.save() + return user + + +class OAuthAPIJWTAuthentication(authentication.BaseAuthentication): def authenticate(self, request): auth_header = get_authorization_header(request) if not auth_header: @@ -42,45 +123,57 @@ def authenticate(self, request): return None # attempt other configured authentication methods token = auth_header[1] # get JWT public key - public_key = "" + cert_dn = request.headers.get("X-Ssl-Client-Dn") + if not cert_dn: + raise exceptions.AuthenticationFailed("No client certificate DN found!") + cert_uid = get_uid_from_dn(cert_dn) + decoded_token = validate_token(token, cert_uid) + user = get_or_create_user_from_token(decoded_token) + logger.info("user from token: " + str(user.email)) + return (user, decoded_token) + + +class OAuthSessionAuthentication(authentication.BaseAuthentication): + """ + Use OAuth session for authentication. + """ + + def authenticate(self, request): + """ + Returns a `User` if the request session currently has a logged in user. + Otherwise returns `None`. + """ + + if not "oauth_token" in request.session: + return None + + token = request.session["oauth_token"] + access_token = token["access_token"].encode("utf-8") + cert_dn = request.headers.get("X-Ssl-Client-Dn") + if not cert_dn: + raise exceptions.AuthenticationFailed("No client certificate DN found!") + cert_uid = get_uid_from_dn(cert_dn) try: - with open(settings.PATH_TO_JWT_PUBLIC_KEY) as public_key_file: - public_key = public_key_file.read() - except Exception as e: - logger.error(e) - if not public_key: - error = exceptions.AuthenticationFailed( - "Unable to get public key to decode jwt" - ) - logger.error(error) + decoded_token = validate_token(access_token, cert_uid) + if "client_id" not in decoded_token: + logger.debug("No client_id in token") + raise exceptions.AuthenticationFailed("No client_id in token") + decoded_client_id = decoded_token["client_id"] + request_client_id = settings.CLIENT_ID + if decoded_client_id != request_client_id: + logger.debug( + f"client_id from token {decoded_client_id} does not match request client_id {request_client_id}" + ) + # https://tools.ietf.org/html/draft-ietf-oauth-security-topics-16#section-2.3 + raise exceptions.AuthenticationFailed( + "Access token was not issued to this client!" + ) + except Exception as error: + del request.session["oauth_token"] raise error - try: - # decode JWT token - # verifies jwt signature using RS256 algorithm and public key - # requires exp claim to verify token is not expired - # decodes and returns base64 encoded payload - decoded_key = jwt.decode( - token, - public_key, - verify=True, - algorithms="RS256", - options={"require": ["exp"], "verify_exp": True}, - ) - except ExpiredSignatureError as e: - logger.error(e) - raise exceptions.AuthenticationFailed("Token is expired!") - except InvalidSignatureError as e: - logger.error(e) - raise exceptions.AuthenticationFailed("Unable to verify token!") - except Exception as e: - logger.error(e) - raise exceptions.AuthenticationFailed(f"Unable to decode token! {e}") - jwt_username = decoded_key["user_name"] - user_model = get_user_model() - user = None - try: - user = user_model.objects.get(username=jwt_username) - except user_model.DoesNotExist: - user = user_model.objects.create_user(username=jwt_username) - user.save() - return (user, decoded_key) + except: + del request.session["oauth_token"] + raise Exception("Error occurred validating token!") + user = get_or_create_user_from_token(decoded_token) + logger.info("user from token: " + str(user.email)) + return (user, decoded_token) diff --git a/src/authentication/oauth.py b/src/authentication/oauth.py index 23f47f8f..78e99c93 100644 --- a/src/authentication/oauth.py +++ b/src/authentication/oauth.py @@ -37,7 +37,6 @@ def get_oauth_token(): verify=verify_ssl, ) oauth.close() - logger.debug("Response from oauth.fetch_token: " + str(token)) return token except Exception: raise diff --git a/src/authentication/oauth_urls.py b/src/authentication/oauth_urls.py new file mode 100644 index 00000000..e480bf2c --- /dev/null +++ b/src/authentication/oauth_urls.py @@ -0,0 +1,12 @@ +from django.conf import settings +from django.contrib.auth import views +from django.urls import path + +from authentication.views import oauth_login_callback, oauth_login_view + +urlpatterns = ( + path("oauth2/", oauth_login_view, name="oauth-login"), + path(f"oauth2/code/{settings.FQDN}", oauth_login_callback, name="oauth-callback"), + # https://github.com/encode/django-rest-framework/blob/master/rest_framework/urls.py + path("oauth2/logout/", views.LogoutView.as_view(), name="oauth-logout"), +) diff --git a/src/authentication/permissions.py b/src/authentication/permissions.py index 40c55ec6..f8725857 100644 --- a/src/authentication/permissions.py +++ b/src/authentication/permissions.py @@ -1,13 +1,23 @@ +import logging + from rest_framework import permissions -from .auth import jwt_request_has_required_role, oauth_jwt_authentication_enabled +from .auth import ( + jwt_request_has_required_role, + oauth_jwt_authentication_enabled, + oauth_session_authentication_enabled, +) + +logger = logging.getLogger(__name__) -class RequiredJWTRolePermissionOrIsSuperuser(permissions.BasePermission): +class JWTRoleOrIsSuperuser(permissions.BasePermission): message = "User missing required role" def has_permission(self, request, view): - if oauth_jwt_authentication_enabled and jwt_request_has_required_role(request): + if ( + oauth_jwt_authentication_enabled or oauth_session_authentication_enabled + ) and jwt_request_has_required_role(request): return True if request.user.is_superuser: return True diff --git a/src/authentication/tests/jwt_content_example.json b/src/authentication/tests/jwt_content_example.json index 5e5f2b4f..d9091460 100644 --- a/src/authentication/tests/jwt_content_example.json +++ b/src/authentication/tests/jwt_content_example.json @@ -9,7 +9,7 @@ "id": null, "uid": "", "altSecurityIdenties": null, - "email": "sensor01", + "email": "sensor01@example.com", "firstname": "sensor01", "lastname": "", "cn": "sensor01", @@ -31,5 +31,5 @@ "ROLE_MANAGER" ], "jti": "e4271916-bfe0-4028-b372-dc05c4882c88", - "client_id": "sensor01" + "client_id": "sensor01.sms.internal" } diff --git a/src/authentication/tests/test_jwt_auth.py b/src/authentication/tests/test_jwt_auth.py index 7074dcc6..cf7d661f 100644 --- a/src/authentication/tests/test_jwt_auth.py +++ b/src/authentication/tests/test_jwt_auth.py @@ -2,10 +2,11 @@ import json import os import secrets +import uuid from datetime import datetime, timedelta +from tempfile import NamedTemporaryFile import jwt -import pem import pytest from cryptography.x509 import load_pem_x509_certificate from django import conf @@ -14,6 +15,7 @@ from authentication.auth import oauth_jwt_authentication_enabled from authentication.models import User +from authentication.tests.utils import get_test_public_private_key from sensor import V1 pytestmark = pytest.mark.skipif( @@ -22,54 +24,20 @@ ) -TEST_JWT_PUBLIC_KEY_FILE = os.path.join(conf.settings.CERTS_DIR, "test/test_pubkey.pem") -TEST_JWT_PRIVATE_KEY_FILE = os.path.join( - conf.settings.CERTS_DIR, "test/test_private_key.pem" -) -PRIVATE_KEY = None -PUBLIC_KEY = None +# PRIVATE_KEY, PUBLIC_KEY = get_test_public_private_key() jwt_content_file = os.path.join( os.path.dirname(os.path.abspath(__file__)), "jwt_content_example.json" ) with open(jwt_content_file) as token_file: TOKEN_CONTENT = json.load(token_file) -with open(TEST_JWT_PRIVATE_KEY_FILE, "rb") as pem_file: - certs = pem.parse(pem_file.read()) - -for cert in certs: - byte_data = cert.as_bytes() - if type(cert) in [pem.RSAPublicKey, pem.Certificate]: - x509_cert = load_pem_x509_certificate(byte_data) - for attribute in x509_cert.subject: - # check commonName (oid = 2.5.4.3) - if ( - attribute.oid.dotted_string == "2.5.4.3" - and attribute.value == "sensor01" - ): - PUBLIC_KEY = cert - elif type(cert) in [pem.RSAPrivateKey, pem.PrivateKey]: - PRIVATE_KEY = cert - else: - raise Exception(f"not checking for type = {type(cert)}") - -BAD_PRIVATE_KEY_FILE = os.path.join( - conf.settings.CERTS_DIR, "test/test_bad_private_key.pem" -) -BAD_PRIVATE_KEY = None -with open(BAD_PRIVATE_KEY_FILE, "rb") as pem_file: - certs = pem.parse(pem_file.read()) - for cert in certs: - byte_data = cert.as_bytes() - if type(cert) in [pem.RSAPrivateKey, pem.PrivateKey]: - BAD_PRIVATE_KEY = cert -BAD_PUBLIC_KEY_FILE = os.path.join(conf.settings.CERTS_DIR, "test/test_bad_pubkey.pem") +BAD_PRIVATE_KEY, BAD_PUBLIC_KEY = get_test_public_private_key() one_min = timedelta(minutes=1) one_day = timedelta(days=1) -def get_token_payload(authorities=["ROLE_MANAGER"], exp=None): +def get_token_payload(authorities=["ROLE_MANAGER"], exp=None, client_id=None, uid=None): token_payload = TOKEN_CONTENT.copy() current_datetime = datetime.now() if not exp: @@ -81,8 +49,20 @@ def get_token_payload(authorities=["ROLE_MANAGER"], exp=None): for authority in authorities: token_payload["userDetails"]["authorities"].append({"authority": authority}) token_payload["userDetails"]["enabled"] = True + if not uid: + uid = str(uuid.uuid4()) + token_payload["userDetails"]["uid"] = uid token_payload["authorities"] = authorities - return token_payload + if client_id: + token_payload["client_id"] = client_id + return token_payload, uid + + +def get_headers(uuid, token): + return { + "Authorization": f"Bearer {token}", + "X-Ssl-Client-Dn": f"UID={uuid},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test", + } @pytest.mark.django_db @@ -93,33 +73,27 @@ def test_no_token_unauthorized(live_server): @pytest.mark.django_db -def test_token_no_roles_unauthorized(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload(authorities=[]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_token_no_roles_unauthorized(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=[]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 403 + assert response.json()["detail"] == "User missing required role" @pytest.mark.django_db -def test_token_role_manager_accepted(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload() - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_token_role_manager_accepted(live_server, jwt_keys): + token_payload, uid = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 200 -def test_bad_token_forbidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE +def test_bad_token_forbidden(live_server): client = RequestsClient() token = ( secrets.token_urlsafe(28) @@ -128,268 +102,219 @@ def test_bad_token_forbidden(settings, live_server): + "." + secrets.token_urlsafe(525) ) - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {token}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers("test_uid", token)) print(f"headers: {response.request.headers}") assert response.status_code == 403 + assert "Unable to decode token!" in response.json()["detail"] @pytest.mark.django_db -def test_token_expired_1_day_forbidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE +def test_token_expired_1_day_forbidden(live_server, jwt_keys): current_datetime = datetime.now() - token_payload = get_token_payload(exp=(current_datetime - one_day).timestamp()) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + token_payload, uid = get_token_payload(exp=(current_datetime - one_day).timestamp()) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 403 + assert response.json()["detail"] == "Token is expired!" @pytest.mark.django_db -def test_bad_private_key_forbidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload() - encoded = jwt.encode(token_payload, str(BAD_PRIVATE_KEY), algorithm="RS256") +def test_bad_private_key_forbidden(live_server): + token_payload, uid = get_token_payload() + encoded = jwt.encode( + token_payload, str(BAD_PRIVATE_KEY.decode("utf-8")), algorithm="RS256" + ) utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 403 + assert response.json()["detail"] == "Unable to verify token!" @pytest.mark.django_db -def test_bad_public_key_forbidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = BAD_PUBLIC_KEY_FILE - token_payload = get_token_payload() - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") - utf8_bytes = encoded.decode("utf-8") - client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) - assert response.status_code == 403 +def test_bad_public_key_forbidden(settings, live_server, jwt_keys): + with NamedTemporaryFile() as jwt_public_key_file: + jwt_public_key_file.write(BAD_PUBLIC_KEY) + jwt_public_key_file.flush() + settings.PATH_TO_JWT_PUBLIC_KEY = jwt_public_key_file.name + token_payload, uid = get_token_payload() + encoded = jwt.encode( + token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = client.get( + f"{live_server.url}", headers=get_headers(uid, utf8_bytes) + ) + assert response.status_code == 403 + assert response.json()["detail"] == "Unable to verify token!" @pytest.mark.django_db -def test_token_expired_1_min_forbidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE +def test_token_expired_1_min_forbidden(live_server, jwt_keys): current_datetime = datetime.now() - token_payload = get_token_payload(exp=(current_datetime - one_min).timestamp()) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + token_payload, uid = get_token_payload(exp=(current_datetime - one_min).timestamp()) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 403 + assert response.json()["detail"] == "Token is expired!" @pytest.mark.django_db -def test_token_expires_in_1_min_accepted(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE +def test_token_expires_in_1_min_accepted(live_server, jwt_keys): current_datetime = datetime.now() - token_payload = get_token_payload(exp=(current_datetime + one_min).timestamp()) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + token_payload, uid = get_token_payload(exp=(current_datetime + one_min).timestamp()) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 200 @pytest.mark.django_db -def test_token_role_user_forbidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_token_role_user_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 403 + assert response.json()["detail"] == "User missing required role" @pytest.mark.django_db -def test_token_role_user_required_role_accepted(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE +def test_token_role_user_required_role_accepted(settings, live_server, jwt_keys): settings.REQUIRED_ROLE = "ROLE_USER" - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 200 @pytest.mark.django_db -def test_token_mulitple_roles_accepted(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload( +def test_token_multiple_roles_accepted(live_server, jwt_keys): + token_payload, uid = get_token_payload( authorities=["ROLE_MANAGER", "ROLE_USER", "ROLE_ITS"] ) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 200 @pytest.mark.django_db -def test_token_mulitple_roles_forbidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload( +def test_token_mulitple_roles_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload( authorities=["ROLE_SENSOR", "ROLE_USER", "ROLE_ITS"] ) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 403 @pytest.mark.django_db -def test_urls_unauthorized(settings, live_server, user): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_urls_unauthorized(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() + headers = get_headers(uid, utf8_bytes) capabilities = reverse("capabilities", kwargs=V1) - response = client.get( - f"{live_server.url}{capabilities}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{capabilities}", headers=headers) assert response.status_code == 403 schedule_list = reverse("schedule-list", kwargs=V1) - response = client.get( - f"{live_server.url}{schedule_list}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{schedule_list}", headers=headers) assert response.status_code == 403 status = reverse("status", kwargs=V1) - response = client.get( - f"{live_server.url}{status}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}{status}", headers=headers) assert response.status_code == 403 task_root = reverse("task-root", kwargs=V1) - response = client.get( - f"{live_server.url}{task_root}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{task_root}", headers=headers) assert response.status_code == 403 task_results_overview = reverse("task-results-overview", kwargs=V1) - response = client.get( - f"{live_server.url}{task_results_overview}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) assert response.status_code == 403 upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) - response = client.get( - f"{live_server.url}{upcoming_tasks}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) assert response.status_code == 403 user_list = reverse("user-list", kwargs=V1) - response = client.get( - f"{live_server.url}{user_list}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{user_list}", headers=headers) assert response.status_code == 403 @pytest.mark.django_db -def test_urls_authorized(settings, live_server, admin_user): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_urls_authorized(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_MANAGER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() + headers = get_headers(uid, utf8_bytes) capabilities = reverse("capabilities", kwargs=V1) - response = client.get( - f"{live_server.url}{capabilities}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{capabilities}", headers=headers) assert response.status_code == 200 schedule_list = reverse("schedule-list", kwargs=V1) - response = client.get( - f"{live_server.url}{schedule_list}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{schedule_list}", headers=headers) assert response.status_code == 200 status = reverse("status", kwargs=V1) - response = client.get( - f"{live_server.url}{status}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}{status}", headers=headers) assert response.status_code == 200 task_root = reverse("task-root", kwargs=V1) - response = client.get( - f"{live_server.url}{task_root}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{task_root}", headers=headers) assert response.status_code == 200 task_results_overview = reverse("task-results-overview", kwargs=V1) - response = client.get( - f"{live_server.url}{task_results_overview}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) assert response.status_code == 200 upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) - response = client.get( - f"{live_server.url}{upcoming_tasks}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) assert response.status_code == 200 user_list = reverse("user-list", kwargs=V1) - response = client.get( - f"{live_server.url}{user_list}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{user_list}", headers=headers) assert response.status_code == 200 @pytest.mark.django_db -def test_user_cannot_view_user_detail(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(sensor01_token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_user_cannot_view_user_detail(live_server, jwt_keys): + sensor01_token_payload, sensor01_uid = get_token_payload( + authorities=["ROLE_MANAGER"] + ) + encoded = jwt.encode( + sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - # authenticating with "ROLE_MANAGER" creates user if does not already exist response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} + f"{live_server.url}", headers=get_headers(sensor01_uid, utf8_bytes) ) assert response.status_code == 200 - sensor02_token_payload = get_token_payload(authorities=["ROLE_USER"]) + sensor02_token_payload, sensor02_uid = get_token_payload(authorities=["ROLE_USER"]) sensor02_token_payload["user_name"] = "sensor02" - encoded = jwt.encode(sensor02_token_payload, str(PRIVATE_KEY), algorithm="RS256") + encoded = jwt.encode( + sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) utf8_bytes = encoded.decode("utf-8") client = RequestsClient() @@ -398,27 +323,24 @@ def test_user_cannot_view_user_detail(settings, live_server): kws.update(V1) user_detail = reverse("user-detail", kwargs=kws) response = client.get( - f"{live_server.url}{user_detail}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, + f"{live_server.url}{user_detail}", headers=get_headers(sensor02_uid, utf8_bytes) ) assert response.status_code == 403 @pytest.mark.django_db -def test_user_cannot_view_user_detail_role_change(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(sensor01_token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_user_cannot_view_user_detail_role_change(live_server, jwt_keys): + sensor01_token_payload, uid = get_token_payload(authorities=["ROLE_MANAGER"]) + encoded = jwt.encode( + sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - # authenticating with "ROLE_MANAGER" creates user if does not already exist - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) assert response.status_code == 200 - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() @@ -427,52 +349,51 @@ def test_user_cannot_view_user_detail_role_change(settings, live_server): kws.update(V1) user_detail = reverse("user-detail", kwargs=kws) response = client.get( - f"{live_server.url}{user_detail}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, + f"{live_server.url}{user_detail}", headers=get_headers(uid, utf8_bytes) ) assert response.status_code == 403 @pytest.mark.django_db -def test_admin_can_view_user_detail(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_admin_can_view_user_detail(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_MANAGER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - # authenticating with "ROLE_MANAGER" creates user if does not already exist - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + headers = get_headers(uid, utf8_bytes) + response = client.get(f"{live_server.url}", headers=headers) assert response.status_code == 200 sensor01_user = User.objects.get(username=token_payload["user_name"]) kws = {"pk": sensor01_user.pk} kws.update(V1) user_detail = reverse("user-detail", kwargs=kws) - response = client.get( - f"{live_server.url}{user_detail}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{user_detail}", headers=headers) assert response.status_code == 200 @pytest.mark.django_db -def test_admin_can_view_other_user_detail(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(sensor01_token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_admin_can_view_other_user_detail(live_server, jwt_keys): + sensor01_token_payload, sensor01_uid = get_token_payload( + authorities=["ROLE_MANAGER"] + ) + encoded = jwt.encode( + sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - # authenticating with "ROLE_MANAGER" creates user if does not already exist response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} + f"{live_server.url}", headers=get_headers(sensor01_uid, utf8_bytes) ) assert response.status_code == 200 - sensor02_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) + sensor02_token_payload, sensor02_uid = get_token_payload( + authorities=["ROLE_MANAGER"] + ) sensor02_token_payload["user_name"] = "sensor02" - encoded = jwt.encode(sensor02_token_payload, str(PRIVATE_KEY), algorithm="RS256") + encoded = jwt.encode( + sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) utf8_bytes = encoded.decode("utf-8") client = RequestsClient() @@ -481,23 +402,19 @@ def test_admin_can_view_other_user_detail(settings, live_server): kws.update(V1) user_detail = reverse("user-detail", kwargs=kws) response = client.get( - f"{live_server.url}{user_detail}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, + f"{live_server.url}{user_detail}", headers=get_headers(sensor02_uid, utf8_bytes) ) assert response.status_code == 200 @pytest.mark.django_db -def test_token_hidden(settings, live_server): - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") +def test_token_hidden(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_MANAGER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") client = RequestsClient() - # authenticating with "ROLE_MANAGER" creates user if does not already exist - response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} - ) + headers = get_headers(uid, utf8_bytes) + response = client.get(f"{live_server.url}", headers=headers) assert response.status_code == 200 sensor01_user = User.objects.get(username=token_payload["user_name"]) @@ -505,10 +422,7 @@ def test_token_hidden(settings, live_server): kws.update(V1) user_detail = reverse("user-detail", kwargs=kws) client = RequestsClient() - response = client.get( - f"{live_server.url}{user_detail}", - headers={"Authorization": f"Bearer {utf8_bytes}"}, - ) + response = client.get(f"{live_server.url}{user_detail}", headers=headers) assert response.status_code == 200 assert ( response.json()["auth_token"] @@ -517,11 +431,10 @@ def test_token_hidden(settings, live_server): @pytest.mark.django_db -def test_change_token_role_bad_signature(settings, live_server): +def test_change_token_role_bad_signature(live_server, jwt_keys): """Make sure token modified after it was signed is rejected""" - settings.PATH_TO_JWT_PUBLIC_KEY = TEST_JWT_PUBLIC_KEY_FILE - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(PRIVATE_KEY), algorithm="RS256") + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") utf8_bytes = encoded.decode("utf-8") first_period = utf8_bytes.find(".") second_period = utf8_bytes.find(".", first_period + 1) @@ -556,6 +469,95 @@ def test_change_token_role_bad_signature(settings, live_server): ) client = RequestsClient() response = client.get( - f"{live_server.url}", headers={"Authorization": f"Bearer {modified_token}"} + f"{live_server.url}", headers=get_headers(uid, modified_token) + ) + assert response.status_code == 403 + assert response.json()["detail"] == "Unable to verify token!" + + +@pytest.mark.django_db +def test_jwt_uid_missing(live_server, jwt_keys): + token_payload, uid = get_token_payload() + token_payload["userDetails"].pop("uid") + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) + assert response.status_code == 403 + assert "Unable to decode token!" in response.json()["detail"] + + token_payload, uid = get_token_payload() + token_payload["userDetails"]["uid"] = None + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) + + token_payload, uid = get_token_payload() + token_payload["userDetails"]["uid"] = "" + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) + + +def test_header_uid_missing(live_server, jwt_keys): + token_payload, _ = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = client.get( + f"{live_server.url}", headers={"Authorization": f"Bearer {utf8_bytes}"} + ) + assert response.status_code == 403 + assert response.json()["detail"] == "No client certificate DN found!" + + response = client.get( + f"{live_server.url}", + headers={"Authorization": f"Bearer {utf8_bytes}", "X-Ssl-Client-Dn": None}, + ) + assert response.status_code == 403 + assert response.json()["detail"] == "No client certificate DN found!" + + response = client.get( + f"{live_server.url}", + headers={"Authorization": f"Bearer {utf8_bytes}", "X-Ssl-Client-Dn": ""}, + ) + assert response.status_code == 403 + assert response.json()["detail"] == "No client certificate DN found!" + + +def test_header_jwt_uid_mismatch(live_server, jwt_keys): + token_payload, uid = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = client.get( + f"{live_server.url}", headers=get_headers("test_uid", utf8_bytes) ) assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) + + token_payload["userDetails"]["uid"] = "test_uid" + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_headers(uid, utf8_bytes)) + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) diff --git a/src/authentication/tests/test_oauth_session.py b/src/authentication/tests/test_oauth_session.py new file mode 100644 index 00000000..b6ff622f --- /dev/null +++ b/src/authentication/tests/test_oauth_session.py @@ -0,0 +1,730 @@ +import base64 +import json +import secrets +import urllib +from datetime import datetime +from tempfile import NamedTemporaryFile +from unittest.mock import patch + +import jwt +import oauthlib +import pytest +import requests_mock +from django.conf import settings +from django.test.testcases import SimpleTestCase +from rest_framework.reverse import reverse +from rest_framework.test import APIClient, RequestsClient + +from authentication.auth import oauth_session_authentication_enabled +from authentication.models import User +from authentication.tests.test_jwt_auth import ( + BAD_PRIVATE_KEY, + BAD_PUBLIC_KEY, + get_token_payload, + one_day, + one_min, +) +from sensor import V1 +from sensor.settings import ( + CLIENT_ID, + CLIENT_SECRET, + FQDN, + OAUTH_AUTHORIZATION_URL, + OAUTH_TOKEN_URL, +) + +pytestmark = pytest.mark.skipif( + not oauth_session_authentication_enabled, + reason="OAuth JWT authentication is not enabled!", +) + + +@pytest.mark.django_db +def test_oauth_login_view(): + client = APIClient() + response = client.get(reverse("oauth-login")) + assert response.wsgi_request.session["oauth_state"] + oauth_callback_path = reverse("oauth-callback") + state = response.wsgi_request.session["oauth_state"] + url_redirect = f"{OAUTH_AUTHORIZATION_URL}?response_type=code&client_id={CLIENT_ID}&redirect_uri=https://{FQDN}{oauth_callback_path}&state={state}" + SimpleTestCase().assertRedirects( + response, url_redirect, fetch_redirect_response=False + ) + + +@pytest.mark.django_db +def test_oauth_login_callback(jwt_keys): + token_payload, uid = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + test_state = "test_state" + client = APIClient() + session = client.session + session["oauth_state"] = test_state + session.save() + response = None + with requests_mock.Mocker() as m: + m.post(OAUTH_TOKEN_URL, json={"access_token": utf8_bytes}) + oauth_callback_url = reverse("oauth-callback") + response = client.get( + f"{oauth_callback_url}?code=test_code&state={test_state}", + HTTP_X_SSL_CLIENT_DN=f"UID={uid},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test", + ) + assert response.wsgi_request.session["oauth_token"]["access_token"] == utf8_bytes + SimpleTestCase().assertRedirects( + response, reverse("api-root", kwargs=V1), fetch_redirect_response=False + ) + response = client.get( + reverse("api-root", kwargs=V1), + HTTP_X_SSL_CLIENT_DN=f"UID={uid},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test", + ) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_oauth_login_callback_bad_state(jwt_keys): + token_payload, uid = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + test_state = "test_state" + client = APIClient() + session = client.session + session["oauth_state"] = test_state + session.save() + with requests_mock.Mocker() as m: + m.post(OAUTH_TOKEN_URL, json={"access_token": utf8_bytes}) + oauth_callback_url = reverse("oauth-callback") + with pytest.raises(oauthlib.oauth2.rfc6749.errors.MismatchingStateError): + client.get( + f"{oauth_callback_url}?code=test_code&state=some_state", + HTTP_X_SSL_CLIENT_DN=f"UID={uid},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test", + ) + response = client.get(reverse("api-root", kwargs=V1)) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_oauth_login_callback_no_token(): + test_state = "test_state" + client = APIClient() + session = client.session + session["oauth_state"] = test_state + session.save() + with requests_mock.Mocker() as m: + m.post(OAUTH_TOKEN_URL, text="") + oauth_callback_url = reverse("oauth-callback") + with pytest.raises(oauthlib.oauth2.rfc6749.errors.MissingTokenError): + client.get( + f"{oauth_callback_url}?code=test_code&state=test_state", + HTTP_X_SSL_CLIENT_DN="UID=test_uid,CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test", + ) + response = client.get(reverse("api-root", kwargs=V1)) + assert response.status_code == 403 + + +# From test_jwt_auth.py +def get_headers(uid): + return { + "X-Ssl-Client-Dn": f"UID={uid},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test" + } + + +def get_oauth_authorized_client(token, live_server): + def auth_callback(request, context): + if f"{request.scheme}://{request.hostname}{request.path}".startswith( + OAUTH_AUTHORIZATION_URL + ): + query_start_index = request.path_url.find("?") + query = request.path_url[query_start_index + 1 :] + if "response_type=code" in query: + if f"client_id={CLIENT_ID}" in query: + for query_param in query.split("&"): + if query_param.startswith("state="): + equals_index = query_param.find("=") + state = query_param[equals_index + 1 :] + _oauth_callback_url = reverse("oauth-callback") + context.headers = { + "Location": f"{live_server.url}{_oauth_callback_url}?code=test_code&state={state}" + } + return "" + + with requests_mock.Mocker(real_http=True) as m: + _oauth_callback_url = reverse("oauth-callback") + _oauth_callback_url = f"https://{settings.FQDN}{_oauth_callback_url}" + _oauth_callback_url = urllib.parse.quote(_oauth_callback_url, safe="") + m.post(OAUTH_TOKEN_URL, json={"access_token": token}) + url_redirect = ( + OAUTH_AUTHORIZATION_URL + "?response_type=code&client_id=" + CLIENT_ID + ) + m.get(url_redirect, status_code=307, text=auth_callback) + client = RequestsClient() + login_path = reverse("oauth-login") + url = f"{live_server.url}{login_path}" + response = client.get(url, allow_redirects=False) # sensor login + assert response.is_redirect == True + assert response.is_permanent_redirect == False + response = client.get( # authserver login + response.headers["Location"], allow_redirects=False + ) + assert response.is_redirect == True + assert response.is_permanent_redirect == False + assert reverse("oauth-callback") in response.headers["Location"] + response = client.get( # sensor callback + response.headers["Location"], allow_redirects=False + ) + assert response.is_redirect == True + assert response.is_permanent_redirect == False + location = response.headers["Location"] + assert reverse("api-root", kwargs=V1) in location + oauth_token_url_found = False + for item in m.request_history: + if OAUTH_TOKEN_URL in item.url: + oauth_token_url_found = True + assert ( + f"grant_type=authorization_code&code=test_code&redirect_uri={_oauth_callback_url}" + in item.text + ) + auth_header = item.headers["Authorization"] + auth_method = auth_header.split()[0] + assert auth_method.lower() == "basic" + basic_auth_encode = auth_header.split()[1] + basic_auth_decode = base64.b64decode(basic_auth_encode) + assert basic_auth_decode.decode() == f"{CLIENT_ID}:{CLIENT_SECRET}" + assert oauth_token_url_found + return client + + +def perform_oauth_test(token, uid, live_server, test_url): + client = get_oauth_authorized_client(token, live_server) + response = client.get(test_url, headers=get_headers(uid)) + return response + + +@pytest.mark.django_db +def test_no_token_unauthorized(live_server): + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_headers("test_uid")) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_token_no_roles_unauthorized(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=[]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_token_role_manager_accepted(live_server, jwt_keys): + token_payload, uid = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_logout(live_server, jwt_keys): + token_payload, uid = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + response = client.get(f"{live_server.url}", headers=get_headers(uid)) + assert response.status_code == 200 + logout_url = reverse("oauth-logout") + response = client.get(f"{live_server.url}{logout_url}", headers=get_headers(uid)) + response.raise_for_status() + response = client.get(f"{live_server.url}", headers=get_headers(uid)) + assert response.status_code == 403 + + +def test_bad_token_forbidden(live_server): + token = ( + secrets.token_urlsafe(28) + + "." + + secrets.token_urlsafe(679) + + "." + + secrets.token_urlsafe(525) + ) + response = perform_oauth_test(token, "test_uid", live_server, f"{live_server.url}") + assert response.status_code == 403 + assert "Unable to decode token!" in response.json()["detail"] + + +@pytest.mark.django_db +def test_token_expired_1_day_forbidden(live_server, jwt_keys): + current_datetime = datetime.now() + token_payload, uid = get_token_payload(exp=(current_datetime - one_day).timestamp()) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "Token is expired!" + + +@pytest.mark.django_db +def test_bad_private_key_forbidden(live_server): + token_payload, uid = get_token_payload() + encoded = jwt.encode( + token_payload, str(BAD_PRIVATE_KEY.decode("utf-8")), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "Unable to verify token!" + + +@pytest.mark.django_db +def test_bad_public_key_forbidden(settings, live_server, jwt_keys): + with NamedTemporaryFile() as bad_public_key_file: + bad_public_key_file.write(BAD_PUBLIC_KEY) + bad_public_key_file.flush() + settings.PATH_TO_JWT_PUBLIC_KEY = bad_public_key_file.name + token_payload, uid = get_token_payload() + encoded = jwt.encode( + token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test( + utf8_bytes, uid, live_server, f"{live_server.url}" + ) + assert response.status_code == 403 + assert response.json()["detail"] == "Unable to verify token!" + + +@pytest.mark.django_db +def test_token_expired_1_min_forbidden(live_server, jwt_keys): + current_datetime = datetime.now() + token_payload, uid = get_token_payload(exp=(current_datetime - one_min).timestamp()) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "Token is expired!" + + +@pytest.mark.django_db +def test_token_expires_in_1_min_accepted(live_server, jwt_keys): + current_datetime = datetime.now() + token_payload, uid = get_token_payload(exp=(current_datetime + one_min).timestamp()) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_token_role_user_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "User missing required role" + + +@pytest.mark.django_db +def test_token_role_user_required_role_accepted(settings, live_server, jwt_keys): + settings.REQUIRED_ROLE = "ROLE_USER" + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_token_multiple_roles_accepted(live_server, jwt_keys): + token_payload, uid = get_token_payload( + authorities=["ROLE_MANAGER", "ROLE_USER", "ROLE_ITS"] + ) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_token_multiple_roles_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload( + authorities=["ROLE_SENSOR", "ROLE_USER", "ROLE_ITS"] + ) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_urls_unauthorized(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + + capabilities = reverse("capabilities", kwargs=V1) + response = client.get(f"{live_server.url}{capabilities}", headers=get_headers(uid)) + assert response.status_code == 403 + + schedule_list = reverse("schedule-list", kwargs=V1) + response = client.get(f"{live_server.url}{schedule_list}", headers=get_headers(uid)) + assert response.status_code == 403 + + status = reverse("status", kwargs=V1) + response = client.get(f"{live_server.url}{status}", headers=get_headers(uid)) + assert response.status_code == 403 + + task_root = reverse("task-root", kwargs=V1) + response = client.get(f"{live_server.url}{task_root}", headers=get_headers(uid)) + assert response.status_code == 403 + + task_results_overview = reverse("task-results-overview", kwargs=V1) + response = client.get( + f"{live_server.url}{task_results_overview}", headers=get_headers(uid) + ) + assert response.status_code == 403 + + upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) + response = client.get( + f"{live_server.url}{upcoming_tasks}", headers=get_headers(uid) + ) + assert response.status_code == 403 + + user_list = reverse("user-list", kwargs=V1) + response = client.get(f"{live_server.url}{user_list}", headers=get_headers(uid)) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_urls_authorized(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_MANAGER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + + capabilities = reverse("capabilities", kwargs=V1) + response = client.get(f"{live_server.url}{capabilities}", headers=get_headers(uid)) + assert response.status_code == 200 + + schedule_list = reverse("schedule-list", kwargs=V1) + response = client.get(f"{live_server.url}{schedule_list}", headers=get_headers(uid)) + assert response.status_code == 200 + + status = reverse("status", kwargs=V1) + response = client.get(f"{live_server.url}{status}", headers=get_headers(uid)) + assert response.status_code == 200 + + task_root = reverse("task-root", kwargs=V1) + response = client.get(f"{live_server.url}{task_root}", headers=get_headers(uid)) + assert response.status_code == 200 + + task_results_overview = reverse("task-results-overview", kwargs=V1) + response = client.get( + f"{live_server.url}{task_results_overview}", headers=get_headers(uid) + ) + assert response.status_code == 200 + + upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) + response = client.get( + f"{live_server.url}{upcoming_tasks}", headers=get_headers(uid) + ) + assert response.status_code == 200 + + user_list = reverse("user-list", kwargs=V1) + response = client.get(f"{live_server.url}{user_list}", headers=get_headers(uid)) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_user_cannot_view_user_detail(live_server, jwt_keys): + sensor01_token_payload, sensor01_uid = get_token_payload( + authorities=["ROLE_MANAGER"] + ) + encoded = jwt.encode( + sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + client_user1 = get_oauth_authorized_client(utf8_bytes, live_server) + response = client_user1.get(f"{live_server.url}", headers=get_headers(sensor01_uid)) + assert response.status_code == 200 + + sensor02_token_payload, sensor02_uid = get_token_payload(authorities=["ROLE_USER"]) + sensor02_token_payload["user_name"] = "sensor02" + encoded = jwt.encode( + sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + client_user2 = get_oauth_authorized_client(utf8_bytes, live_server) + + sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) + kws = {"pk": sensor01_user.pk} + kws.update(V1) + user_detail = reverse("user-detail", kwargs=kws) + response = client_user2.get( + f"{live_server.url}{user_detail}", headers=get_headers(sensor02_uid) + ) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_user_cannot_view_user_detail_role_change(live_server, jwt_keys): + sensor01_token_payload, sensor01_uid = get_token_payload( + authorities=["ROLE_MANAGER"] + ) + encoded = jwt.encode( + sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + response = client.get(f"{live_server.url}", headers=get_headers(sensor01_uid)) + assert response.status_code == 200 + + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client_new_token = get_oauth_authorized_client(utf8_bytes, live_server) + + sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) + kws = {"pk": sensor01_user.pk} + kws.update(V1) + user_detail = reverse("user-detail", kwargs=kws) + response = client_new_token.get( + f"{live_server.url}{user_detail}", headers=get_headers(uid) + ) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_admin_can_view_user_detail(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_MANAGER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + response = client.get(f"{live_server.url}", headers=get_headers(uid)) + assert response.status_code == 200 + + sensor01_user = User.objects.get(username=token_payload["user_name"]) + kws = {"pk": sensor01_user.pk} + kws.update(V1) + user_detail = reverse("user-detail", kwargs=kws) + response = client.get(f"{live_server.url}{user_detail}", headers=get_headers(uid)) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_admin_can_view_other_user_detail(live_server, jwt_keys): + sensor01_token_payload, sensor01_uid = get_token_payload( + authorities=["ROLE_MANAGER"] + ) + encoded = jwt.encode( + sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + client_user1 = get_oauth_authorized_client(utf8_bytes, live_server) + response = client_user1.get(f"{live_server.url}", headers=get_headers(sensor01_uid)) + assert response.status_code == 200 + + sensor02_token_payload, sensor02_uid = get_token_payload( + authorities=["ROLE_MANAGER"] + ) + sensor02_token_payload["user_name"] = "sensor02" + encoded = jwt.encode( + sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" + ) + utf8_bytes = encoded.decode("utf-8") + client_user2 = get_oauth_authorized_client(utf8_bytes, live_server) + + sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) + kws = {"pk": sensor01_user.pk} + kws.update(V1) + user_detail = reverse("user-detail", kwargs=kws) + response = client_user2.get( + f"{live_server.url}{user_detail}", headers=get_headers(sensor02_uid) + ) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_token_hidden(live_server, jwt_keys): + token_payload, uid = get_token_payload(authorities=["ROLE_MANAGER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + response = client.get(f"{live_server.url}", headers=get_headers(uid)) + assert response.status_code == 200 + + sensor01_user = User.objects.get(username=token_payload["user_name"]) + kws = {"pk": sensor01_user.pk} + kws.update(V1) + user_detail = reverse("user-detail", kwargs=kws) + response = client.get(f"{live_server.url}{user_detail}", headers=get_headers(uid)) + assert response.status_code == 200 + assert ( + response.json()["auth_token"] + == "rest_framework.authentication.TokenAuthentication is not enabled" + ) + + +@pytest.mark.django_db +def test_change_token_role_bad_signature(live_server, jwt_keys): + """Make sure token modified after it was signed is rejected""" + token_payload, uid = get_token_payload(authorities=["ROLE_USER"]) + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + first_period = utf8_bytes.find(".") + second_period = utf8_bytes.find(".", first_period + 1) + payload = utf8_bytes[first_period + 1 : second_period] + payload_bytes = payload.encode("utf-8") + # must be multiple of 4 for b64decode + for i in range(len(payload_bytes) % 4): + payload_bytes = payload_bytes + b"=" + decoded = base64.b64decode(payload_bytes) + payload_str = decoded.decode("utf-8") + payload_data = json.loads(payload_str) + payload_data["user_name"] = "sensor013" + payload_data["authorities"] = ["ROLE_MANAGER"] + payload_data["userDetails"]["authorities"] = [{"authority": "ROLE_MANAGER"}] + payload_str = json.dumps(payload_data) + encoded = base64.b64encode(payload_str.encode("utf-8")) + modified_payload = encoded.decode("utf-8") + # remove padding + if modified_payload.endswith("="): + last_padded_index = len(modified_payload) - 1 + for i in range(len(modified_payload) - 1, -1, -1): + if modified_payload[i] != "=": + last_padded_index = i + break + modified_payload = modified_payload[: last_padded_index + 1] + modified_token = ( + utf8_bytes[:first_period] + + "." + + modified_payload + + "." + + utf8_bytes[second_period + 1 :] + ) + client = get_oauth_authorized_client(modified_token, live_server) + response = client.get(f"{live_server.url}", headers=get_headers(uid)) + assert response.status_code == 403 + assert response.json()["detail"] == "Unable to verify token!" + + +@pytest.mark.django_db +def test_bad_client_id_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload(client_id="bad_client_id") + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "Access token was not issued to this client!" + + +@pytest.mark.django_db +def test_no_client_id_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload() + del token_payload["client_id"] + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert "No client_id in token" == response.json()["detail"] + + +@pytest.mark.django_db +def test_client_id_none_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload() + token_payload["client_id"] = None + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "Access token was not issued to this client!" + + +@pytest.mark.django_db +def test_client_id_empty_forbidden(live_server, jwt_keys): + token_payload, uid = get_token_payload() + token_payload["client_id"] = "" + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "Access token was not issued to this client!" + + +@pytest.mark.django_db +def test_jwt_uid_missing(live_server, jwt_keys): + token_payload, uid = get_token_payload() + token_payload["userDetails"].pop("uid") + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert "Unable to decode token!" in response.json()["detail"] + + token_payload, uid = get_token_payload() + token_payload["userDetails"]["uid"] = None + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) + + token_payload, uid = get_token_payload() + token_payload["userDetails"]["uid"] = "" + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = RequestsClient() + response = perform_oauth_test(utf8_bytes, uid, live_server, f"{live_server.url}") + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) + + +def test_header_uid_missing(live_server, jwt_keys): + token_payload, _ = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + response = client.get(f"{live_server.url}") + assert response.status_code == 403 + assert response.json()["detail"] == "No client certificate DN found!" + + response = client.get(f"{live_server.url}", headers={"X-Ssl-Client-Dn": None}) + assert response.status_code == 403 + assert response.json()["detail"] == "No client certificate DN found!" + + response = client.get(f"{live_server.url}", headers={"X-Ssl-Client-Dn": ""}) + assert response.status_code == 403 + assert response.json()["detail"] == "No client certificate DN found!" + + +def test_header_jwt_uid_mismatch(live_server, jwt_keys): + token_payload, uid = get_token_payload() + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + response = client.get(f"{live_server.url}", headers=get_headers("test_uid")) + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) + + token_payload["userDetails"]["uid"] = "test_uid" + encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + client = get_oauth_authorized_client(utf8_bytes, live_server) + response = client.get(f"{live_server.url}", headers=get_headers(uid)) + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "Unable to decode token! JWT DN does not match client certificate DN!" + ) diff --git a/src/authentication/tests/test_uid.py b/src/authentication/tests/test_uid.py new file mode 100644 index 00000000..fb125458 --- /dev/null +++ b/src/authentication/tests/test_uid.py @@ -0,0 +1,43 @@ +from authentication.auth import get_uid_from_dn + + +def test_get_uid_from_dn_comma(): + assert "12345671111111" == get_uid_from_dn( + "UID=12345671111111,CN=JUSTIN HAZE,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US" + ) + assert "12345671111111" == get_uid_from_dn( + "CN=JUSTIN HAZE,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US,UID=12345671111111" + ) + assert "1234567" == get_uid_from_dn( + "UID=1234567,CN=JUSTIN HAZE,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US" + ) + assert "1234567" == get_uid_from_dn( + "CN=JUSTIN HAZE,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US,UID=1234567" + ) + + +def test_get_uid_from_dn_plus(): + assert "12345671111111" == get_uid_from_dn( + "UID=12345671111111+CN=JUSTIN HAZE,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US" + ) + assert "12345671111111" == get_uid_from_dn( + "CN=JUSTIN HAZE+UID=12345671111111,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US" + ) + assert "12345671111111" == get_uid_from_dn( + "OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US,UID=12345671111111+CN=JUSTIN HAZE" + ) + assert "12345671111111" == get_uid_from_dn( + "OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US,CN=JUSTIN HAZE+UID=12345671111111" + ) + assert "1234567" == get_uid_from_dn( + "UID=1234567+CN=JUSTIN HAZE,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US" + ) + assert "1234567" == get_uid_from_dn( + "CN=JUSTIN HAZE+UID=1234567,OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US" + ) + assert "1234567" == get_uid_from_dn( + "OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US,UID=1234567+CN=JUSTIN HAZE" + ) + assert "1234567" == get_uid_from_dn( + "OU=National Telecommunication and Information Administration,OU=Department of Commerce,O=U.S. Government,C=US,CN=JUSTIN HAZE+UID=1234567" + ) diff --git a/src/authentication/tests/utils.py b/src/authentication/tests/utils.py new file mode 100644 index 00000000..3ee92b5d --- /dev/null +++ b/src/authentication/tests/utils.py @@ -0,0 +1,21 @@ +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + + +def get_test_public_private_key(): + """Creates public/private key pair for testing + https://stackoverflow.com/a/39126754 + """ + key = rsa.generate_private_key( + backend=default_backend(), public_exponent=65537, key_size=4096 + ) + private_key = key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + public_key = key.public_key().public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo + ) + return private_key, public_key diff --git a/src/authentication/views.py b/src/authentication/views.py index 5aa1db5f..62988a8c 100644 --- a/src/authentication/views.py +++ b/src/authentication/views.py @@ -1,8 +1,28 @@ +""" +OAuth 2 Views +https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html +""" +import base64 +import logging +from urllib.parse import urlparse + +from django.conf import settings +from django.http.response import HttpResponseRedirect +from requests_oauthlib.oauth2_session import OAuth2Session +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from sensor import V1 from .models import User from .serializers import UserDetailsSerializer +logger = logging.getLogger(__name__) + class UserDetailsListView(ListCreateAPIView): """View user details and create users.""" @@ -14,3 +34,94 @@ class UserDetailsListView(ListCreateAPIView): class UserDetailsView(RetrieveUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserDetailsSerializer + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def oauth_login_view(request): + """ + Step 1: User Authorization. + + Redirect the user/resource owner to the OAuth provider (i.e. Github) + using an URL with a few key OAuth parameters. + + # Step 2: User authorization, this happens on the provider. + + https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html + """ + authserver = OAuth2Session( + settings.CLIENT_ID, + redirect_uri="https://" + settings.FQDN + reverse("oauth-callback"), + ) + logger.debug("OAUTH_AUTHORIZATION_URL = " + settings.OAUTH_AUTHORIZATION_URL) + authorization_url, state = authserver.authorization_url( + settings.OAUTH_AUTHORIZATION_URL + ) + logger.debug("authorization_url = " + str(authorization_url)) + # State is used to prevent CSRF, keep this for later. + request.session["oauth_state"] = state + return HttpResponseRedirect(authorization_url) + + +# def log_fetch_token(response): +# request = response.request +# url = request.url +# body = request.body +# headers = request.headers +# logger.debug(f"fetch token body = {body}") +# logger.debug(f"fetch_token url = {url}") +# for key, value in headers.items(): +# logger.debug(f"fetch_token header {key} = {value}") +# auth_header = request.headers["Authorization"] +# auth_method = auth_header.split()[0] +# logger.debug(f"auth method = {auth_method.lower()}") +# basic_auth_encode = auth_header.split()[1] +# basic_auth_decode = base64.b64decode(basic_auth_encode) +# logger.debug(f"decoded auth header = {basic_auth_decode.decode()}") +# logger.debug("-------response-----------") +# logger.debug(f"response.url = {response.url}") +# logger.debug(f"response.text = {response.text}") +# return response + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def oauth_login_callback(request): + """Step 3: Retrieving an access token. + + The user has been redirected back from the provider to your registered + callback URL. With this redirection comes an authorization code included + in the redirect URL. We will use that to obtain an access token. + https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html + """ + if not "oauth_state" in request.session: + return Response( + "'oauth_state' missing from session", status=status.HTTP_403_FORBIDDEN + ) + state = request.session["oauth_state"] + del request.session["oauth_state"] # state can only be used once + authorization_host = urlparse(settings.OAUTH_AUTHORIZATION_URL).hostname + token_host = urlparse(settings.OAUTH_TOKEN_URL).hostname + if authorization_host != token_host: + raise Exception( + "OAUTH_AUTHORIZATION_URL and OAUTH_TOKEN_URL must use the same host!" + ) + + authserver = OAuth2Session( + settings.CLIENT_ID, + state=state, + redirect_uri="https://" + settings.FQDN + reverse("oauth-callback"), + ) + authserver.cert = settings.PATH_TO_CLIENT_CERT + logger.debug("OAUTH_TOKEN_URL = " + settings.OAUTH_TOKEN_URL) + logger.debug("authorization_response = " + request.build_absolute_uri()) + # authserver.register_compliance_hook("access_token_response", log_fetch_token) + token = authserver.fetch_token( + settings.OAUTH_TOKEN_URL, + client_secret=settings.CLIENT_SECRET, + authorization_response=request.build_absolute_uri(), + verify=settings.PATH_TO_VERIFY_CERT, + ) + + request.session["oauth_token"] = token + return HttpResponseRedirect(reverse("api-root", kwargs=V1)) diff --git a/src/conftest.py b/src/conftest.py index be18183d..eda6639b 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -1,14 +1,25 @@ import shutil +import tempfile +from collections import namedtuple +import jwt import pytest from django.conf import settings from django.test.client import Client import actions import scheduler +from authentication.auth import oauth_session_authentication_enabled from authentication.models import User +from authentication.tests.test_jwt_auth import get_token_payload +from authentication.tests.utils import get_test_public_private_key +from sensor.tests.scos_test_client import UID, SCOSTestClient from tasks.models import TaskResult +PRIVATE_KEY, PUBLIC_KEY = get_test_public_private_key() +Keys = namedtuple("KEYS", ["private_key", "public_key"]) +keys = Keys(PRIVATE_KEY.decode("utf-8"), PUBLIC_KEY.decode("utf-8")) + @pytest.fixture(autouse=True) def cleanup_db(db): @@ -60,8 +71,17 @@ def user(db): @pytest.fixture def user_client(db, user): """A Django test client logged in as a normal user""" - client = Client() - client.login(username=user.username, password=user.password) + client = SCOSTestClient() + if oauth_session_authentication_enabled: + token_payload, _ = get_token_payload(authorities=["ROLE_USER"], uid=UID) + encoded = jwt.encode(token_payload, str(keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + session = client.session + session["oauth_token"] = {} + session["oauth_token"]["access_token"] = utf8_bytes + session.save() + else: + client.login(username=user.username, password=user.password) return client @@ -86,8 +106,35 @@ def alt_user(db): @pytest.fixture def alt_user_client(db, alt_user): """A Django test client logged in as a normal user""" - client = Client() - client.login(username=alt_user.username, password=alt_user.password) + client = SCOSTestClient() + if oauth_session_authentication_enabled: + token_payload, _ = get_token_payload(authorities=["ROLE_USER"], uid=UID) + encoded = jwt.encode(token_payload, str(keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + session = client.session + session["oauth_token"] = {} + session["oauth_token"]["access_token"] = utf8_bytes + session.save() + else: + client.login(username=alt_user.username, password=alt_user.password) + + return client + + +@pytest.fixture +def admin_client(db, django_user_model, admin_user): + """A Django test client logged in as an admin user.""" + client = SCOSTestClient() + if oauth_session_authentication_enabled: + token_payload, _ = get_token_payload(uid=UID) + encoded = jwt.encode(token_payload, str(keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + session = client.session + session["oauth_token"] = {} + session["oauth_token"]["access_token"] = utf8_bytes + session.save() + else: + client.login(username=admin_user.username, password="password") return client @@ -121,9 +168,25 @@ def alt_admin_user(db, django_user_model, django_username_field): @pytest.fixture def alt_admin_client(db, alt_admin_user): """A Django test client logged in as an admin user.""" - from django.test.client import Client - - client = Client() - client.login(username=alt_admin_user.username, password="password") + client = SCOSTestClient() + if oauth_session_authentication_enabled: + token_payload, _ = get_token_payload(uid=UID) + encoded = jwt.encode(token_payload, str(keys.private_key), algorithm="RS256") + utf8_bytes = encoded.decode("utf-8") + session = client.session + session["oauth_token"] = {} + session["oauth_token"]["access_token"] = utf8_bytes + session.save() + else: + client.login(username=alt_admin_user.username, password="password") return client + + +@pytest.fixture(autouse=True) +def jwt_keys(settings): + with tempfile.NamedTemporaryFile() as jwt_public_key_file: + jwt_public_key_file.write(PUBLIC_KEY) + jwt_public_key_file.flush() + settings.PATH_TO_JWT_PUBLIC_KEY = jwt_public_key_file.name + yield keys diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index 7ebab1b5..9090e6ca 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -1,13 +1,12 @@ -rrequirements.txt -black==18.9b0 +black==21.5b1 flake8==3.7.7 flake8-bugbear==19.3.0 isort==4.3.20 jedi==0.13.3 jsonschema==3.0.1 mkdocs==1.0.4 -pem~=20.1.0 pre-commit~=1.16.1 pytest-cov==2.7.1 pytest-django==4.1.0 diff --git a/src/requirements.txt b/src/requirements.txt index ec6a784b..09d4d70d 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -22,7 +22,7 @@ raven==6.10.0 requests-futures==0.9.9 requests-mock==1.6.0 requests_oauthlib~=1.3.0 -#scos_actions @ git+https://github.com/NTIA/scos-actions@master#egg=scos_actions +scos_actions @ git+https://github.com/NTIA/scos-actions@master#egg=scos_actions scos_usrp @ git+https://github.com/NTIA/scos-usrp@master#egg=scos_usrp six==1.15.0 typing==3.6.6 diff --git a/src/schedule/migrations/0001_initial.py b/src/schedule/migrations/0001_initial.py index abd81345..4b0aa6c5 100644 --- a/src/schedule/migrations/0001_initial.py +++ b/src/schedule/migrations/0001_initial.py @@ -9,7 +9,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] diff --git a/src/schedule/migrations/0002_auto_20210113_2151.py b/src/schedule/migrations/0002_auto_20210113_2151.py new file mode 100644 index 00000000..c3c74f07 --- /dev/null +++ b/src/schedule/migrations/0002_auto_20210113_2151.py @@ -0,0 +1,137 @@ +# Generated by Django 2.2.13 on 2021-01-13 21:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("schedule", "0001_initial")] + + operations = [ + migrations.AlterField( + model_name="scheduleentry", + name="action", + field=models.CharField( + choices=[ + ( + "acquire_iq_700MHz_ATT_DL", + "acquire_iq_700MHz_ATT_DL - Capture time-domain IQ samples at the following 1 frequencies: 739.00 MHz.", + ), + ( + "acquire_iq_700MHz_ATT_UL", + "acquire_iq_700MHz_ATT_UL - Capture time-domain IQ samples at the following 1 frequencies: 709.00 MHz.", + ), + ( + "acquire_iq_700MHz_FirstNet_DL", + "acquire_iq_700MHz_FirstNet_DL - Capture time-domain IQ samples at the following 1 frequencies: 763.00 MHz.", + ), + ( + "acquire_iq_700MHz_FirstNet_UL", + "acquire_iq_700MHz_FirstNet_UL - Capture time-domain IQ samples at the following 1 frequencies: 793.00 MHz.", + ), + ( + "acquire_iq_700MHz_P-SafetyNB_DL", + "acquire_iq_700MHz_P-SafetyNB_DL - Capture time-domain IQ samples at the following 1 frequencies: 772.00 MHz.", + ), + ( + "acquire_iq_700MHz_P-SafetyNB_UL", + "acquire_iq_700MHz_P-SafetyNB_UL - Capture time-domain IQ samples at the following 1 frequencies: 802.00 MHz.", + ), + ( + "acquire_iq_700MHz_T-Mobile_DL", + "acquire_iq_700MHz_T-Mobile_DL - Capture time-domain IQ samples at the following 1 frequencies: 731.50 MHz.", + ), + ( + "acquire_iq_700MHz_T-Mobile_UL", + "acquire_iq_700MHz_T-Mobile_UL - Capture time-domain IQ samples at the following 1 frequencies: 700.50 MHz.", + ), + ( + "acquire_iq_700MHz_Verizon_DL", + "acquire_iq_700MHz_Verizon_DL - Capture time-domain IQ samples at the following 1 frequencies: 751.00 MHz.", + ), + ( + "acquire_iq_700MHz_Verizon_UL", + "acquire_iq_700MHz_Verizon_UL - Capture time-domain IQ samples at the following 1 frequencies: 782.00 MHz.", + ), + ( + "acquire_m4s_700MHz_ATT_DL", + "acquire_m4s_700MHz_ATT_DL - Apply m4s detector over 300 1024-pt FFTs at 739.00 MHz.", + ), + ( + "acquire_m4s_700MHz_ATT_UL", + "acquire_m4s_700MHz_ATT_UL - Apply m4s detector over 300 1024-pt FFTs at 709.00 MHz.", + ), + ( + "acquire_m4s_700MHz_FirstNet_DL", + "acquire_m4s_700MHz_FirstNet_DL - Apply m4s detector over 300 1024-pt FFTs at 763.00 MHz.", + ), + ( + "acquire_m4s_700MHz_FirstNet_UL", + "acquire_m4s_700MHz_FirstNet_UL - Apply m4s detector over 300 1024-pt FFTs at 793.00 MHz.", + ), + ( + "acquire_m4s_700MHz_P-SafetyNB_DL", + "acquire_m4s_700MHz_P-SafetyNB_DL - Apply m4s detector over 300 1024-pt FFTs at 772.00 MHz.", + ), + ( + "acquire_m4s_700MHz_P-SafetyNB_UL", + "acquire_m4s_700MHz_P-SafetyNB_UL - Apply m4s detector over 300 1024-pt FFTs at 802.00 MHz.", + ), + ( + "acquire_m4s_700MHz_T-Mobile_DL", + "acquire_m4s_700MHz_T-Mobile_DL - Apply m4s detector over 300 1024-pt FFTs at 731.50 MHz.", + ), + ( + "acquire_m4s_700MHz_T-Mobile_UL", + "acquire_m4s_700MHz_T-Mobile_UL - Apply m4s detector over 300 1024-pt FFTs at 700.50 MHz.", + ), + ( + "acquire_m4s_700MHz_Verizon_DL", + "acquire_m4s_700MHz_Verizon_DL - Apply m4s detector over 300 1024-pt FFTs at 751.00 MHz.", + ), + ( + "acquire_m4s_700MHz_Verizon_UL", + "acquire_m4s_700MHz_Verizon_UL - Apply m4s detector over 300 1024-pt FFTs at 782.00 MHz.", + ), + ("logger", 'logger - Log the message "running test {name}/{tid}".'), + ( + "monitor_usrp", + "monitor_usrp - Monitor USRP connection and restart container if unreachable.", + ), + ( + "survey_700MHz_band_10dB_1000ms_iq", + "survey_700MHz_band_10dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.", + ), + ( + "survey_700MHz_band_10dB_80ms_iq", + "survey_700MHz_band_10dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.", + ), + ( + "survey_700MHz_band_20dB_1000ms_iq", + "survey_700MHz_band_20dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.", + ), + ( + "survey_700MHz_band_20dB_80ms_iq", + "survey_700MHz_band_20dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.", + ), + ( + "survey_700MHz_band_40dB_1000ms_iq", + "survey_700MHz_band_40dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.", + ), + ( + "survey_700MHz_band_40dB_80ms_iq", + "survey_700MHz_band_40dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.", + ), + ( + "survey_700MHz_band_iq", + "survey_700MHz_band_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.", + ), + ( + "sync_gps", + "sync_gps - Query the GPS and syncronize time and location.", + ), + ], + help_text="[Required] The name of the action to be scheduled", + max_length=50, + ), + ) + ] diff --git a/src/sensor/renderer.py b/src/sensor/renderer.py new file mode 100644 index 00000000..757bdfc5 --- /dev/null +++ b/src/sensor/renderer.py @@ -0,0 +1,13 @@ +from django.conf import settings +from rest_framework.renderers import BrowsableAPIRenderer + + +class BrowsableAPIRendererWithCustomAuth(BrowsableAPIRenderer): + def get_context(self, *args, **kwargs): + """ + Adds authentication method to browsable API context, + adapted from https://bradmontgomery.net/blog/disabling-forms-django-rest-frameworks-browsable-api/ + """ + context = super().get_context(*args, **kwargs) + context["AUTHENTICATION"] = settings.AUTHENTICATION + return context diff --git a/src/sensor/settings.py b/src/sensor/settings.py index 355a6f9a..98830be5 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -99,14 +99,6 @@ ALLOWED_HOSTS = env.str("DOMAINS").split() + env.str("IPS").split() POSTGRES_PASSWORD = env("POSTGRES_PASSWORD") -SESSION_COOKIE_SECURE = IN_DOCKER -CSRF_COOKIE_SECURE = IN_DOCKER - -SESSION_COOKIE_AGE = 900 # seconds -SESSION_EXPIRE_SECONDS = 900 # seconds -SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True -SESSION_TIMEOUT_REDIRECT = "/api/auth/logout/?next=/api/v1/" - # Application definition API_TITLE = "SCOS Sensor API" @@ -215,16 +207,15 @@ REST_FRAMEWORK = { "EXCEPTION_HANDLER": "sensor.exceptions.exception_handler", - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework.authentication.SessionAuthentication", - ), + "DEFAULT_AUTHENTICATION_CLASSES": None, "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", - "authentication.permissions.RequiredJWTRolePermissionOrIsSuperuser", + "authentication.permissions.JWTRoleOrIsSuperuser", ), "DEFAULT_RENDERER_CLASSES": ( "rest_framework.renderers.JSONRenderer", - "rest_framework.renderers.BrowsableAPIRenderer", + "sensor.renderer.BrowsableAPIRendererWithCustomAuth" + # "rest_framework.renderers.BrowsableAPIRenderer", ), "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning", "DEFAULT_VERSION": "v1", # this should always point to latest stable api @@ -237,19 +228,6 @@ "URL_FIELD_NAME": "self", # RFC 42867 } -AUTHENTICATION = env("AUTHENTICATION", default="") -if AUTHENTICATION == "JWT": - REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( - "authentication.auth.OAuthJWTAuthentication", - "rest_framework.authentication.SessionAuthentication", - ) -else: - REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( - "rest_framework.authentication.TokenAuthentication", - "rest_framework.authentication.SessionAuthentication", - ) - - # https://drf-yasg.readthedocs.io/en/stable/settings.html SWAGGER_SETTINGS = { "SECURITY_DEFINITIONS": {}, @@ -258,7 +236,24 @@ "VALIDATOR_URL": None, } -if AUTHENTICATION == "JWT": +SESSION_COOKIE_SECURE = IN_DOCKER +SESSION_EXPIRE_AT_BROWSER_CLOSE = True +CSRF_COOKIE_SECURE = IN_DOCKER +USE_X_FORWARDED_HOST = IN_DOCKER +USE_X_FORWARDED_PORT = IN_DOCKER +SESSION_COOKIE_SAMESITE = "Strict" +SESSION_COOKIE_AGE = 900 # seconds +SESSION_EXPIRE_SECONDS = 900 # seconds +SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True +# https://docs.djangoproject.com/en/3.0/ref/middleware/#module-django.middleware.security +SECURE_REFERRER_POLICY = "same-origin" # needs django 3.0+ + +AUTHENTICATION = env("AUTHENTICATION", default="") +if AUTHENTICATION == "OAUTH": + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( + "authentication.auth.OAuthAPIJWTAuthentication", + "authentication.auth.OAuthSessionAuthentication", + ) SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["oAuth2JWT"] = { "type": "oauth2", "description": ( @@ -270,7 +265,12 @@ ), "flows": {"password": {"scopes": {}}}, # scopes are not used } + SESSION_TIMEOUT_REDIRECT = "oauth-logout" else: + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( + "rest_framework.authentication.TokenAuthentication", + "rest_framework.authentication.SessionAuthentication", + ) SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["token"] = { "type": "apiKey", "description": ( @@ -290,6 +290,7 @@ "name": "Token", "in": "header", } + SESSION_TIMEOUT_REDIRECT = "rest_framework:logout" # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases @@ -384,6 +385,7 @@ PASSWORD = CLIENT_SECRET OAUTH_TOKEN_URL = env("OAUTH_TOKEN_URL", default="") +OAUTH_AUTHORIZATION_URL = env("OAUTH_AUTHORIZATION_URL", default="") CERTS_DIR = path.join(CONFIG_DIR, "certs") # Sensor certificate with private key used as client cert PATH_TO_CLIENT_CERT = env("PATH_TO_CLIENT_CERT", default="") diff --git a/src/sensor/tests/scos_test_client.py b/src/sensor/tests/scos_test_client.py new file mode 100644 index 00000000..8e08762f --- /dev/null +++ b/src/sensor/tests/scos_test_client.py @@ -0,0 +1,74 @@ +from django.conf import settings +from django.test import Client +from django.test.client import MULTIPART_CONTENT + +UID = "test_uid" + + +class SCOSTestClient(Client): + def get(self, path, data=None, follow=False, secure=False, **extra): + if settings.AUTHENTICATION == "OAUTH": + extra[ + "HTTP_X_SSL_CLIENT_DN" + ] = f"UID={UID},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test" + return super().get(path, data, follow, secure, **extra) + + def post( + self, + path, + data=None, + content_type=MULTIPART_CONTENT, + follow=False, + secure=False, + **extra, + ): + if settings.AUTHENTICATION == "OAUTH": + extra[ + "HTTP_X_SSL_CLIENT_DN" + ] = f"UID={UID},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test" + return super().post(path, data, content_type, follow, secure, **extra) + + def delete( + self, + path, + data="", + content_type="application/octet-stream", + follow=False, + secure=False, + **extra, + ): + if settings.AUTHENTICATION == "OAUTH": + extra[ + "HTTP_X_SSL_CLIENT_DN" + ] = f"UID={UID},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test" + return super().delete(path, data, content_type, follow, secure, **extra) + + def put( + self, + path, + data="", + content_type="application/octet-stream", + follow=False, + secure=False, + **extra, + ): + if settings.AUTHENTICATION == "OAUTH": + extra[ + "HTTP_X_SSL_CLIENT_DN" + ] = f"UID={UID},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test" + return super().put(path, data, content_type, follow, secure, **extra) + + def patch( + self, + path, + data="", + content_type="application/octet-stream", + follow=False, + secure=False, + **extra, + ): + if settings.AUTHENTICATION == "OAUTH": + extra[ + "HTTP_X_SSL_CLIENT_DN" + ] = f"UID={UID},CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test" + return super().patch(path, data, content_type, follow, secure, **extra) diff --git a/src/sensor/urls.py b/src/sensor/urls.py index 0613d031..840e3dd9 100644 --- a/src/sensor/urls.py +++ b/src/sensor/urls.py @@ -17,13 +17,13 @@ """ +from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path, re_path from django.views.generic import RedirectView from rest_framework.urlpatterns import format_suffix_patterns -from . import settings from .views import api_v1_root, schema_view # Matches api/v1, api/v2, etc... @@ -57,9 +57,13 @@ path("admin/", admin.site.urls), path("api/", RedirectView.as_view(url="/api/{}/".format(DEFAULT_API_VERSION))), re_path(API_PREFIX, include(api_urlpatterns)), - path("api/auth/", include("rest_framework.urls")), ] +if settings.AUTHENTICATION == "OAUTH": + urlpatterns.append(path("login/", include("authentication.oauth_urls"))) +else: + urlpatterns.append(path("api/auth/", include("rest_framework.urls"))) + if settings.DEBUG: import debug_toolbar diff --git a/src/templates/rest_framework/api.html b/src/templates/rest_framework/api.html index 79af3593..c14a51e7 100644 --- a/src/templates/rest_framework/api.html +++ b/src/templates/rest_framework/api.html @@ -2,6 +2,7 @@ {% block script %} {{ block.super }} +{% if user.is_authenticated %} +{% endif %} {% endblock %} diff --git a/src/templates/rest_framework/base.html b/src/templates/rest_framework/base.html index 8e2aaec4..8e25be46 100644 --- a/src/templates/rest_framework/base.html +++ b/src/templates/rest_framework/base.html @@ -17,8 +17,25 @@ {% if user.is_staff %}
  • Configuration
  • {% endif %} - {% optional_logout request user %} + {% if AUTHENTICATION == "OAUTH" %} + + + {% else %} + {% optional_logout request user %} + {% endif %} {% else %} - {% optional_login request %} + {% if AUTHENTICATION == "OAUTH" %} +
  • Log in
  • + {% else %} + {% optional_login request %} + {% endif %} {% endif %} {% endblock %} diff --git a/src/tox.ini b/src/tox.ini index 22c8e063..b3dae47a 100644 --- a/src/tox.ini +++ b/src/tox.ini @@ -14,14 +14,19 @@ setenv = CALLBACK_AUTHENTICATION=TOKEN [testenv:oauth] -envlist = py36,py37,py38,py39 +#envlist = py36,py37,py38,py39 setenv = - AUTHENTICATION=JWT + AUTHENTICATION=OAUTH CALLBACK_AUTHENTICATION=OAUTH CLIENT_ID=sensor01.sms.internal CLIENT_SECRET=sensor-secret + FQDN = sensor01.sms.internal + OAUTH_AUTHORIZATION_URL=https://test/authserver/oauth/authorize + OAUTH_TOKEN_URL=https://test/authserver/oauth/token PATH_TO_CLIENT_CERT=test/sensor01.pem PATH_TO_VERIFY_CERT=test/scos_test_ca.crt + PATH_TO_JWT_PUBLIC_KEY=test/test_pubkey.pem + OAUTHLIB_INSECURE_TRANSPORT=1 # disable https requirement for testing [testenv:coverage] basepython = python3