diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 52cdee3cb3..3138b50f55 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -71,10 +71,7 @@ data: {{- end -}} {{- if .settings.disabledAPIVersions }} disabledAPIVersions: {{ .settings.disabledAPIVersions }} - {{- end }} - {{- if $.Values.secrets.oauthPublicJwk }} - oauthPublicJwk: /etc/wire/galley/secrets/oauth_ed25519_pub.jwk - {{- end }} + {{- end }} {{- if .settings.featureFlags }} featureFlags: sso: {{ .settings.featureFlags.sso }} diff --git a/charts/galley/templates/secret.yaml b/charts/galley/templates/secret.yaml index 81b1ca205a..354c52fc1e 100644 --- a/charts/galley/templates/secret.yaml +++ b/charts/galley/templates/secret.yaml @@ -14,6 +14,3 @@ data: removal_ed25519.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ed25519 | b64enc | quote }} {{- end -}} {{- end -}} - {{- if .Values.secrets.oauthPublicJwk }} - oauth_ed25519_pub.jwk: {{ .Values.secrets.oauthPublicJwk | b64enc | quote }} - {{- end -}} diff --git a/charts/nginz/templates/conf/_nginx.conf.tpl b/charts/nginz/templates/conf/_nginx.conf.tpl index dba5c08c50..75f32f7a1a 100644 --- a/charts/nginz/templates/conf/_nginx.conf.tpl +++ b/charts/nginz/templates/conf/_nginx.conf.tpl @@ -193,6 +193,7 @@ http { zauth_keystore {{ .Values.nginx_conf.zauth_keystore }}; zauth_acl {{ .Values.nginx_conf.zauth_acl }}; + oauth_key {{ .Values.nginx_conf.oauth_key }}; location /status { zauth off; @@ -258,8 +259,8 @@ http { {{- end }} {{- end }} - {{- if ($location.enable_oauth) }} - oauth on; + {{- if ($location.oauth_scope) }} + oauth_scope {{ $location.oauth_scope }}; {{- end }} {{- if hasKey $location "specific_user_rate_limit" }} @@ -293,6 +294,10 @@ http { proxy_set_header Connection ""; {{ end -}} + {{- if not ($location.disable_zauth) }} + proxy_set_header Authorization ""; + {{- end }} + proxy_set_header Z-Type $zauth_type; proxy_set_header Z-User $zauth_user; proxy_set_header Z-Client $zauth_client; @@ -345,6 +350,7 @@ http { # we need to specify zauth_keystore etc. zauth_keystore {{ .Values.nginx_conf.zauth_keystore }}; zauth_acl {{ .Values.nginx_conf.zauth_acl }}; + oauth_key {{ .Values.nginx_conf.oauth_key }}; listen {{ .Values.config.http.metricsPort }}; diff --git a/charts/nginz/templates/secret.yaml b/charts/nginz/templates/secret.yaml index 12779270f6..b7746d74f7 100644 --- a/charts/nginz/templates/secret.yaml +++ b/charts/nginz/templates/secret.yaml @@ -15,4 +15,5 @@ data: {{- with .Values.secrets }} zauth.conf: {{ .zAuth.publicKeys | b64enc | quote }} basic_auth.txt: {{ .basicAuth | b64enc | quote }} + oauth_ed25519_pub.jwk: {{ .oAuth.publicKeys | b64enc | quote }} {{- end }} diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index cf1630ab51..9cb41104c3 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -30,6 +30,7 @@ nginx_conf: zauth_keystore: /etc/wire/nginz/secrets/zauth.conf zauth_acl: /etc/wire/nginz/conf/zauth.acl basic_auth_file: /etc/wire/nginz/secrets/basic_auth.txt + oauth_key: /etc/wire/nginz/secrets/oauth_ed25519_pub.jwk worker_processes: auto worker_rlimit_nofile: 131072 worker_connections: 65536 @@ -163,7 +164,7 @@ nginx_conf: envs: - staging - path: /self$ # Matches exactly /self - enable_oauth: true + oauth_scope: self envs: - all - path: /self/name @@ -424,11 +425,20 @@ nginx_conf: - all max_body_size: 40m body_buffer_size: 256k + - path: /conversations$ + envs: + - all + doc: true + oauth_scope: conversations + - path: /conversations/([^/]*)/code + envs: + - all + doc: true + oauth_scope: conversations_code - path: /conversations envs: - all doc: true - enable_oauth: true - path: /legalhold/conversations/(.*) envs: - all @@ -496,7 +506,7 @@ nginx_conf: - path: /feature-configs(.*) envs: - all - enable_oauth: true + oauth_scope: feature_configs - path: /mls/welcome envs: - all diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 6681fa50fc..177c91d9d8 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -54,16 +54,6 @@ certificate, is to run the following command: openssl req -nodes -newkey ed25519 -keyout ed25519.pem -out /dev/null -subj / ``` -### Public JWK for OAuth - -Set the path to the public JWK key for OAuth like this: - -```yml -# [galley.yaml] -settings: - oauthPublicJwk: test/resources/oauth/ed25519_public.jwk -``` - ## Feature flags > Also see [Wire docs](https://docs.wire.com/how-to/install/team-feature-settings.html) where some of the feature flags are documented from an operations point of view. diff --git a/hack/bin/oauth_test.sh b/hack/bin/oauth_test.sh index fa1d395869..bc04a780fc 100755 --- a/hack/bin/oauth_test.sh +++ b/hack/bin/oauth_test.sh @@ -79,4 +79,4 @@ echo "access token : $ACCESS_TOKEN" echo "" echo "making a request to /self..." -curl -s -H 'Z-OAUTH: Bearer '"$ACCESS_TOKEN" -H "Content-Type: application/json" localhost:8082/self | jq . +curl -s -H 'Authorization: Bearer '"$ACCESS_TOKEN" -H "Content-Type: application/json" localhost:8080/self | jq diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 4e42674cc3..e770419dc4 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -86,7 +86,7 @@ brig: setDpopTokenExpirationTimeSecs: 300 setEnableMLS: true setOAuthAuthCodeExpirationTimeSecs: 3 # 3 secs - setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks + setOAuthAccessTokenExpirationTimeSecs: 3 # 3 secs setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 @@ -193,12 +193,6 @@ galley: -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c -----END PRIVATE KEY----- - oauthPublicJwk: | - { - "kty": "OKP", - "crv": "Ed25519", - "x": "mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc" - } gundeck: replicaCount: 1 @@ -244,6 +238,13 @@ nginz: zAuth: # this must match the key in brig! publicKeys: 0UW38se1yeoc5bVNEvf5LyrHWGZkyvcGTVilK2geGdU= + oAuth: + publicKeys: | + { + "kty": "OKP", + "crv": "Ed25519", + "x": "mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc" + } proxy: replicaCount: 1 imagePullPolicy: {{ .Values.imagePullPolicy }} diff --git a/libs/libzauth/libzauth-c/Cargo.lock b/libs/libzauth/libzauth-c/Cargo.lock index 7acceb85ae..051bf1a2bb 100644 --- a/libs/libzauth/libzauth-c/Cargo.lock +++ b/libs/libzauth/libzauth-c/Cargo.lock @@ -11,25 +11,339 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + [[package]] name = "asexp" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e368761ce758947307f1c2db1f46077b1aabb5af7f268b6cededd1b52802652" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] +name = "binstring" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85" + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "coarsetime" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46" +dependencies = [ + "libc", + "once_cell", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ct-codecs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12844141594ad74185a926d030f3b605f6a903b4e3fec351f3ea338ac5b7637e" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature 2.0.0", +] + [[package]] name = "ed25519" -version = "1.4.1" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519-compact" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3d382e8464107391c8706b4c14b087808ecb909f6c15c34114bc42e53a9e4c" +dependencies = [ + "ct-codecs", + "getrandom", +] + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha1-compact" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e2440a0078e20c3b68ca01234cea4219f23e64b0c0bdb1200c5550d54239bb" + +[[package]] +name = "hmac-sha256" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4" +checksum = "fc736091aacb31ddaa4cd5f6988b3c21e99913ac846b41f32538c5fae5d71bfe" dependencies = [ - "signature", + "digest", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520c9c3f6040661669bc5c91e551b605a520c8e0a63a766a91a65adef734d151" +dependencies = [ + "digest", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jwt-simple" +version = "0.11.3" +source = "git+https://github.com/wireapp/rust-jwt-simple?tag=v0.11.3-pre.core-crypto-0.6.0#15a69f82288d68b74a75c1364e5d4bf681f1c07b" +dependencies = [ + "anyhow", + "binstring", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand", + "rsa", + "serde", + "serde_json", + "spki", + "thiserror", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a55e0ff3b72c262bcf041d9e97f1b84492b68f1c1a384de2323d3dc9403397" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature 2.0.0", ] [[package]] @@ -37,6 +351,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -44,6 +361,12 @@ version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + [[package]] name = "libsodium-sys" version = "0.2.7" @@ -56,18 +379,199 @@ dependencies = [ "walkdir", ] +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "p256" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c124b3cbce43bcbac68c58ec181d98ed6cc7e6d0aa7c3ba97b2563410b0e55" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630a4a9b2618348ececfae61a4905f564b817063bf2d66cdfc2ced523fe1d2d4" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primeorder" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b54f7131b3dba65a2f414cf5bd25b66d4682e4608610668eae785750ba4c5b2" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.6.0" @@ -85,12 +589,50 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature 1.6.4", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "rustc-serialize" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + [[package]] name = "same-file" version = "1.0.6" @@ -100,17 +642,87 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "serde" -version = "1.0.137" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core", +] [[package]] name = "signature" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "sodiumoxide" @@ -124,6 +736,77 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.3.2" @@ -135,6 +818,66 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + [[package]] name = "winapi" version = "0.3.9" @@ -171,10 +914,15 @@ name = "zauth" version = "3.1.0" dependencies = [ "asexp", + "base64", + "jwt-simple", "lazy_static", "regex", "rustc-serialize", + "serde", + "serde_json", "sodiumoxide", + "thiserror", ] [[package]] @@ -184,3 +932,9 @@ dependencies = [ "libc", "zauth", ] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/libs/libzauth/libzauth-c/Cargo.toml b/libs/libzauth/libzauth-c/Cargo.toml index 73ef35cc2f..9aa25bbf2b 100644 --- a/libs/libzauth/libzauth-c/Cargo.toml +++ b/libs/libzauth/libzauth-c/Cargo.toml @@ -13,3 +13,7 @@ libc = ">= 0.2" [dependencies.zauth] path = "../libzauth" + +[patch.crates-io.jwt-simple] +git = "https://github.com/wireapp/rust-jwt-simple" +tag = "v0.11.3-pre.core-crypto-0.6.0" diff --git a/libs/libzauth/libzauth-c/Makefile b/libs/libzauth/libzauth-c/Makefile index 37e7c72853..228563cbad 100644 --- a/libs/libzauth/libzauth-c/Makefile +++ b/libs/libzauth/libzauth-c/Makefile @@ -36,7 +36,7 @@ install: build-release sed -e "s~<>~$(VERSION)~" \ -e "s~<>~$(PREFIX_INSTALL)~" \ src/libzauth.pc > $(PREFIX_INSTALL)/lib/pkgconfig/libzauth.pc - cp target/release/libzauth.$(LIB_TYPE) $(PREFIX_INSTALL)/lib/ + cp target/debug/libzauth.$(LIB_TYPE) $(PREFIX_INSTALL)/lib/ uninstall: rm -f $(PREFIX_INSTALL)/include/zauth.h diff --git a/libs/libzauth/libzauth-c/src/lib.rs b/libs/libzauth/libzauth-c/src/lib.rs index 12850c4808..9df1dae3f2 100644 --- a/libs/libzauth/libzauth-c/src/lib.rs +++ b/libs/libzauth/libzauth-c/src/lib.rs @@ -20,31 +20,34 @@ extern crate zauth; use libc::size_t; use std::char; +use std::ffi::{CString, NulError}; use std::fs::File; use std::io::{self, BufReader, Read}; +use std::panic::{self, UnwindSafe}; use std::path::Path; use std::ptr; use std::slice; use std::str; -use std::panic::{self, UnwindSafe}; -use zauth::{Acl, Error, Keystore, Token, TokenType, TokenVerification}; use zauth::acl; +use zauth::{ + verify_oauth_token, Acl, Error, Keystore, OauthError, Token, TokenType, TokenVerification, +}; /// Variant of std::try! that returns the unwrapped error. macro_rules! try_unwrap { ($expr:expr) => { match $expr { - Ok(x) => x, - Err(e) => return From::from(e) + Ok(x) => x, + Err(e) => return From::from(e), } - } + }; } #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct Range { ptr: *const u8, - len: size_t + len: size_t, } pub struct ZauthAcl(zauth::Acl); @@ -52,58 +55,70 @@ pub struct ZauthKeystore(zauth::Keystore); pub struct ZauthToken(zauth::Token<'static>); #[no_mangle] -pub extern fn zauth_keystore_open(f: *const u8, n: size_t, s: *mut *mut ZauthKeystore) -> ZauthResult { +pub extern "C" fn zauth_keystore_open( + f: *const u8, + n: size_t, + s: *mut *mut ZauthKeystore, +) -> ZauthResult { if f.is_null() { return ZauthResult::NullArg; } catch_unwind(|| { let bytes = unsafe { slice::from_raw_parts(f, n) }; - let path = try_unwrap!(str::from_utf8(bytes)); + let path = try_unwrap!(str::from_utf8(bytes)); let store = try_unwrap!(Keystore::open(&Path::new(path))); unsafe { - *s= Box::into_raw(Box::new(ZauthKeystore(store))); + *s = Box::into_raw(Box::new(ZauthKeystore(store))); } ZauthResult::Ok }) } #[no_mangle] -pub extern fn zauth_keystore_delete(s: *mut ZauthKeystore) { +pub extern "C" fn zauth_keystore_delete(s: *mut ZauthKeystore) { catch_unwind(|| { - unsafe { Box::from_raw(s); } + unsafe { + drop(Box::from_raw(s)); + } ZauthResult::Ok }); } #[no_mangle] -pub extern fn zauth_acl_open(f: *const u8, n: size_t, a: *mut *mut ZauthAcl) -> ZauthResult { +pub extern "C" fn zauth_acl_open(f: *const u8, n: size_t, a: *mut *mut ZauthAcl) -> ZauthResult { if f.is_null() { return ZauthResult::NullArg; } catch_unwind(|| { let bytes = unsafe { slice::from_raw_parts(f, n) }; - let path = try_unwrap!(str::from_utf8(bytes)); + let path = try_unwrap!(str::from_utf8(bytes)); let mut rdr = BufReader::new(try_unwrap!(File::open(&Path::new(path)))); let mut txt = String::new(); try_unwrap!(rdr.read_to_string(&mut txt)); let acl = try_unwrap!(Acl::from_str(&txt)); unsafe { - *a= Box::into_raw(Box::new(ZauthAcl(acl))); + *a = Box::into_raw(Box::new(ZauthAcl(acl))); } ZauthResult::Ok }) } #[no_mangle] -pub extern fn zauth_acl_delete(a: *mut ZauthAcl) { +pub extern "C" fn zauth_acl_delete(a: *mut ZauthAcl) { catch_unwind(|| { - unsafe { Box::from_raw(a); } + unsafe { + drop(Box::from_raw(a)); + } ZauthResult::Ok }); } #[no_mangle] -pub extern fn zauth_token_parse(cs: *const u8, n: size_t, zt: *mut *mut ZauthToken) -> ZauthResult { +pub extern "C" fn zauth_token_parse( + cs: *const u8, + n: size_t, + zt: *mut *mut ZauthToken, +) -> ZauthResult { if cs.is_null() { return ZauthResult::NullArg; } @@ -119,23 +134,26 @@ pub extern fn zauth_token_parse(cs: *const u8, n: size_t, zt: *mut *mut ZauthTok } #[no_mangle] -pub extern fn zauth_token_verify(t: &mut ZauthToken, s: &ZauthKeystore) -> ZauthResult { +pub extern "C" fn zauth_token_verify(t: &mut ZauthToken, s: &ZauthKeystore) -> ZauthResult { let result = catch_unwind(|| { try_unwrap!(t.0.verify(&s.0)); ZauthResult::Ok }); - unsafe { - match result { - ZauthResult::Ok => t.0.verification = TokenVerification::Verified, - _ => t.0.verification = TokenVerification::Invalid - }; + match result { + ZauthResult::Ok => t.0.verification = TokenVerification::Verified, + _ => t.0.verification = TokenVerification::Invalid, }; result } #[no_mangle] -pub extern -fn zauth_token_allowed(t: &ZauthToken, acl: &ZauthAcl, cp: *const u8, n: size_t, out: *mut u8) -> ZauthResult { +pub extern "C" fn zauth_token_allowed( + t: &ZauthToken, + acl: &ZauthAcl, + cp: *const u8, + n: size_t, + out: *mut u8, +) -> ZauthResult { catch_unwind(|| { let b = unsafe { slice::from_raw_parts(cp, n) }; let s = try_unwrap!(str::from_utf8(b)); @@ -149,12 +167,12 @@ fn zauth_token_allowed(t: &ZauthToken, acl: &ZauthAcl, cp: *const u8, n: size_t, } #[no_mangle] -pub extern fn zauth_token_type(t: &ZauthToken) -> ZauthTokenType { +pub extern "C" fn zauth_token_type(t: &ZauthToken) -> ZauthTokenType { From::from(t.0.token_type) } #[no_mangle] -pub extern fn zauth_token_verification(t: &ZauthToken) -> ZauthTokenVerification { +pub extern "C" fn zauth_token_verification(t: &ZauthToken) -> ZauthTokenVerification { From::from(t.0.verification) } @@ -165,24 +183,32 @@ pub extern fn zauth_token_verification(t: &ZauthToken) -> ZauthTokenVerification //} #[no_mangle] -pub extern fn zauth_token_version(t: &ZauthToken) -> u8 { +pub extern "C" fn zauth_token_version(t: &ZauthToken) -> u8 { t.0.version } #[no_mangle] -pub extern fn zauth_token_lookup(t: &ZauthToken, c: u8) -> Range { +pub extern "C" fn zauth_token_lookup(t: &ZauthToken, c: u8) -> Range { if let Some(k) = char::from_u32(c as u32) { if let Some(s) = t.0.lookup(k) { - return Range { ptr: s.as_ptr(), len: s.len() } + return Range { + ptr: s.as_ptr(), + len: s.len(), + }; } } - Range { ptr: ptr::null(), len: 0 } + Range { + ptr: ptr::null(), + len: 0, + } } #[no_mangle] -pub extern fn zauth_token_delete(t: *mut ZauthToken) { +pub extern "C" fn zauth_token_delete(t: *mut ZauthToken) { catch_unwind(|| { - unsafe { Box::from_raw(t); } + unsafe { + drop(Box::from_raw(t)); + } ZauthResult::Ok }); } @@ -190,25 +216,25 @@ pub extern fn zauth_token_delete(t: *mut ZauthToken) { #[repr(C)] #[derive(Clone, Copy, Debug)] pub enum ZauthTokenType { - User = 0, - Bot = 1, - Access = 2, - Provider = 4, - LegalHoldUser = 5, + User = 0, + Bot = 1, + Access = 2, + Provider = 4, + LegalHoldUser = 5, LegalHoldAccess = 6, - Unknown = 3 + Unknown = 3, } impl From for ZauthTokenType { fn from(t: TokenType) -> ZauthTokenType { match t { - TokenType::User => ZauthTokenType::User, - TokenType::Access => ZauthTokenType::Access, - TokenType::Bot => ZauthTokenType::Bot, - TokenType::Provider => ZauthTokenType::Provider, - TokenType::LegalHoldUser => ZauthTokenType::LegalHoldUser, - TokenType::LegalHoldAccess => ZauthTokenType::LegalHoldAccess, - TokenType::Unknown => ZauthTokenType::Unknown + TokenType::User => ZauthTokenType::User, + TokenType::Access => ZauthTokenType::Access, + TokenType::Bot => ZauthTokenType::Bot, + TokenType::Provider => ZauthTokenType::Provider, + TokenType::LegalHoldUser => ZauthTokenType::LegalHoldUser, + TokenType::LegalHoldAccess => ZauthTokenType::LegalHoldAccess, + TokenType::Unknown => ZauthTokenType::Unknown, } } } @@ -216,32 +242,32 @@ impl From for ZauthTokenType { #[repr(C)] #[derive(Clone, Copy, Debug)] pub enum ZauthResult { - Ok = 0, - Base64Error = 1, - Expired = 2, - InvalidAttr = 3, - IoError = 4, - MissingAttr = 5, - NullArg = 6, - ParseError = 7, + Ok = 0, + Base64Error = 1, + Expired = 2, + InvalidAttr = 3, + IoError = 4, + MissingAttr = 5, + NullArg = 6, + ParseError = 7, SignatureMismatch = 8, - UnknownKey = 9, - Utf8Error = 10, - AclError = 11, - Panic = 99 + UnknownKey = 9, + Utf8Error = 10, + AclError = 11, + Panic = 99, } impl From for ZauthResult { fn from(e: zauth::Error) -> ZauthResult { match e { - Error::Base64 => ZauthResult::Base64Error, - Error::Expired => ZauthResult::Expired, - Error::Invalid(_) => ZauthResult::InvalidAttr, - Error::Io(_) => ZauthResult::IoError, - Error::Missing(_) => ZauthResult::MissingAttr, - Error::Parse => ZauthResult::ParseError, + Error::Base64 => ZauthResult::Base64Error, + Error::Expired => ZauthResult::Expired, + Error::Invalid(_) => ZauthResult::InvalidAttr, + Error::Io(_) => ZauthResult::IoError, + Error::Missing(_) => ZauthResult::MissingAttr, + Error::Parse => ZauthResult::ParseError, Error::SignatureMismatch => ZauthResult::SignatureMismatch, - Error::UnknownKey(_) => ZauthResult::UnknownKey, + Error::UnknownKey(_) => ZauthResult::UnknownKey, } } } @@ -265,10 +291,12 @@ impl From for ZauthResult { } fn catch_unwind(f: F) -> ZauthResult - where F: FnOnce() -> ZauthResult + UnwindSafe { +where + F: FnOnce() -> ZauthResult + UnwindSafe, +{ match panic::catch_unwind(f) { - Ok(x) => x, - Err(_) => ZauthResult::Panic + Ok(x) => x, + Err(_) => ZauthResult::Panic, } } @@ -284,8 +312,197 @@ impl From for ZauthTokenVerification { fn from(t: TokenVerification) -> ZauthTokenVerification { match t { TokenVerification::Verified => ZauthTokenVerification::Verified, - TokenVerification::Invalid => ZauthTokenVerification::Invalid, - TokenVerification::Pending => ZauthTokenVerification::Pending, + TokenVerification::Invalid => ZauthTokenVerification::Invalid, + TokenVerification::Pending => ZauthTokenVerification::Pending, } } } + +// ------------------------------------------------------------------------ +// OAuth + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub enum OAuthResultStatus { + Ok = 0, + InsufficientScope = 1, + NullArg = 2, + IoError = 3, + Utf8Error = 4, + Panic = 99, +} + +pub struct OAuthJwk(String); + +#[repr(C)] +#[derive(Clone, Debug)] +pub struct OAuthResult { + uid: *const libc::c_char, + status: OAuthResultStatus, +} + +fn catch_unwind_with(f: F, r: R) -> R +where + F: FnOnce() -> R + UnwindSafe, +{ + match panic::catch_unwind(f) { + Ok(x) => x, + Err(_) => r, + } +} + +impl From for OAuthResultStatus { + fn from(e: OauthError) -> Self { + match e { + OauthError::JsonError(_) => Self::Panic, + OauthError::JwtSimpleError(_) => Self::Panic, + OauthError::Base64DecodeError(_) => Self::Panic, + OauthError::InvalidJwk => Self::Panic, + OauthError::InvalidJwtNoSubject => Self::Panic, + OauthError::InvalidScope => Self::InsufficientScope, + } + } +} + +impl From for OAuthResultStatus { + fn from(_: io::Error) -> Self { + Self::IoError + } +} + +impl From for OAuthResultStatus { + fn from(_: str::Utf8Error) -> Self { + Self::Utf8Error + } +} + +impl From for OAuthResult { + fn from(_: str::Utf8Error) -> Self { + OAuthResult { + uid: ptr::null(), + status: OAuthResultStatus::Utf8Error, + } + } +} + +impl From for OAuthResult { + fn from(_: NulError) -> Self { + OAuthResult { + uid: ptr::null(), + status: OAuthResultStatus::Panic, + } + } +} + +impl From for OAuthResult { + fn from(e: OauthError) -> Self { + OAuthResult { + uid: ptr::null(), + status: e.into(), + } + } +} + +#[no_mangle] +pub extern "C" fn oauth_key_open( + f: *const u8, + n: size_t, + k: *mut *mut OAuthJwk, +) -> OAuthResultStatus { + if f.is_null() { + return OAuthResultStatus::NullArg; + } + catch_unwind_with( + || { + let bytes = unsafe { slice::from_raw_parts(f, n) }; + let path = try_unwrap!(str::from_utf8(bytes)); + let mut rdr = BufReader::new(try_unwrap!(File::open(&Path::new(path)))); + let mut txt = String::new(); + try_unwrap!(rdr.read_to_string(&mut txt)); + unsafe { + *k = Box::into_raw(Box::new(OAuthJwk(txt))); + } + OAuthResultStatus::Ok + }, + OAuthResultStatus::Panic, + ) +} + +#[no_mangle] +pub extern "C" fn oauth_key_delete(a: *mut OAuthJwk) { + catch_unwind_with( + || { + unsafe { + drop(Box::from_raw(a)); + } + OAuthResultStatus::Ok + }, + OAuthResultStatus::Panic, + ); +} + +#[no_mangle] +pub extern "C" fn oauth_verify_token( + jwk: &OAuthJwk, + token: *const u8, + token_len: size_t, + scope: *const u8, + scope_len: size_t, + method: *const u8, + method_len: size_t, +) -> OAuthResult { + match panic::catch_unwind(|| { + if token.is_null() { + return OAuthResult { + uid: ptr::null(), + status: OAuthResultStatus::NullArg, + }; + } + if scope.is_null() { + return OAuthResult { + uid: ptr::null(), + status: OAuthResultStatus::NullArg, + }; + } + if method.is_null() { + return OAuthResult { + uid: ptr::null(), + status: OAuthResultStatus::NullArg, + }; + } + let bytes = unsafe { slice::from_raw_parts(token, token_len) }; + let token = try_unwrap!(str::from_utf8(bytes)); + let bytes = unsafe { slice::from_raw_parts(scope, scope_len) }; + let scope = try_unwrap!(str::from_utf8(bytes)); + let bytes = unsafe { slice::from_raw_parts(method, method_len) }; + let method = str::from_utf8(bytes).unwrap(); + let subject = try_unwrap!(verify_oauth_token(&jwk.0, token, scope, method)); + let c_str = try_unwrap!(CString::new(subject)); + OAuthResult { + uid: c_str.into_raw(), + status: OAuthResultStatus::Ok, + } + }) { + Ok(x) => x, + Err(_) => OAuthResult { + uid: ptr::null(), + status: OAuthResultStatus::Panic, + }, + } +} + +#[no_mangle] +pub extern "C" fn oauth_result_uid_delete(s: *mut libc::c_char) { + catch_unwind_with( + || { + unsafe { + if s.is_null() { + return OAuthResultStatus::Ok; + } + CString::from_raw(s) + }; + OAuthResultStatus::Ok + }, + OAuthResultStatus::Panic, + ); +} diff --git a/libs/libzauth/libzauth-c/src/zauth.h b/libs/libzauth/libzauth-c/src/zauth.h index 32cbcb38f5..c5cc24ea0b 100644 --- a/libs/libzauth/libzauth-c/src/zauth.h +++ b/libs/libzauth/libzauth-c/src/zauth.h @@ -49,12 +49,31 @@ typedef struct ZauthAcl ZauthAcl; typedef struct ZauthKeystore ZauthKeystore; typedef struct ZauthToken ZauthToken; +typedef enum { + OAUTH_OK = 0, + OAUTH_INSUFFICIENT_SCOPE = 1, + OAUTH_NULL_ARG = 2, + OAUTH_IO_ERROR = 3, + OAUTH_UTF8_ERROR = 4, + OAUTH_PANIC = 99, +} OAuthResultStatus; + +typedef struct OAuthJwk OAuthJwk; + +typedef struct { + char * uid; + OAuthResultStatus status; +} OAuthResult; + ZauthResult zauth_keystore_open(uint8_t const * fname, size_t len, ZauthKeystore **); void zauth_keystore_delete(ZauthKeystore * store); ZauthResult zauth_acl_open(uint8_t const * fname, size_t len, ZauthAcl **); void zauth_acl_delete(ZauthAcl * store); +OAuthResultStatus oauth_key_open(uint8_t const * fname, size_t len, OAuthJwk **); +void oauth_key_delete(OAuthJwk * store); + ZauthResult zauth_token_parse(uint8_t const * str, size_t len, ZauthToken **); ZauthResult zauth_token_verify(ZauthToken const *, ZauthKeystore const *); ZauthTokenType zauth_token_type(ZauthToken const *); @@ -64,6 +83,8 @@ uint8_t zauth_token_version(ZauthToken const *); Range zauth_token_lookup(ZauthToken const *, uint8_t); ZauthResult zauth_token_allowed(ZauthToken const *, ZauthAcl const *, uint8_t const * path, size_t len, uint8_t * result); void zauth_token_delete(ZauthToken *); +OAuthResult oauth_verify_token(OAuthJwk const *, uint8_t const * t, size_t t_len, uint8_t const * s, size_t s_len, uint8_t const * m, size_t m_len); +OAuthResultStatus oauth_result_uid_delete(char *); #ifdef __cplusplus } diff --git a/libs/libzauth/libzauth/Cargo.toml b/libs/libzauth/libzauth/Cargo.toml index 34920bd6f2..49a3a3108f 100644 --- a/libs/libzauth/libzauth/Cargo.toml +++ b/libs/libzauth/libzauth/Cargo.toml @@ -13,6 +13,15 @@ rustc-serialize = ">= 0.3" sodiumoxide = "^0.2.7" regex = "1.6" lazy_static = "1.4" +jwt-simple = "0.11" +serde_json = "1.0" +serde = "1.0" +thiserror = "1.0" +base64 = "0.21" [dev-dependencies] clap = ">= 2.0" + +[patch.crates-io.jwt-simple] +git = "https://github.com/wireapp/rust-jwt-simple" +tag = "v0.11.3-pre.core-crypto-0.6.0" diff --git a/libs/libzauth/libzauth/src/lib.rs b/libs/libzauth/libzauth/src/lib.rs index 9d13e25627..195d06138b 100644 --- a/libs/libzauth/libzauth/src/lib.rs +++ b/libs/libzauth/libzauth/src/lib.rs @@ -20,13 +20,20 @@ extern crate lazy_static; extern crate regex; extern crate rustc_serialize; extern crate sodiumoxide; +extern crate jwt_simple; +extern crate serde_json; +extern crate serde; +extern crate thiserror; +extern crate base64; pub mod acl; pub mod error; pub mod zauth; +pub mod oauth; mod matcher; pub use acl::Acl; pub use error::Error; pub use zauth::{Keystore, Token, TokenType, TokenVerification}; +pub use oauth::{verify_oauth_token, OauthError}; diff --git a/libs/libzauth/libzauth/src/oauth.rs b/libs/libzauth/libzauth/src/oauth.rs new file mode 100644 index 0000000000..723f91810e --- /dev/null +++ b/libs/libzauth/libzauth/src/oauth.rs @@ -0,0 +1,155 @@ +use base64::Engine; +use jwt_simple::prelude::*; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct OAuthToken { + pub scope: String, +} + +pub fn verify_oauth_token( + jwk: &str, + token: &str, + required_scope: &str, + method: &str, +) -> Result { + let jwk = serde_json::from_str::(jwk)?; + let key = try_from_jwk(&jwk)?; + let options = VerificationOptions { + time_tolerance: Some(Duration::from_secs(1)), + ..Default::default() + }; + let claims = key.verify_token::(token, Some(options))?; + let subject = claims.subject.ok_or(OauthError::InvalidJwtNoSubject)?; + verify_scope(&claims.custom.scope, required_scope, method)?; + Ok(subject) +} + +// if method is GET, authorized scopes must contain either read:_, write:_, or admin:_ +// if method is POST, authorized scopes must contain either write:_ or admin:_ +// if method is PUT, authorized scopes must contain either write:_ or admin:_ +// if method is DELETE, authorized scopes must contain admin:_ +// FUTUREWORK: this works for now, but maybe we should consider using a more flexible scope system in the future +// e.g. by using a configuration file with a mapping of scopes to methods and paths +fn verify_scope( + authorized_scopes: &str, + required_scope: &str, + method: &str, +) -> Result<(), OauthError> { + let valid_scopes = match method.to_uppercase().as_str() { + "GET" => Ok(vec!["read", "write", "admin"]), + "POST" => Ok(vec!["write", "admin"]), + "PUT" => Ok(vec!["write", "admin"]), + "DELETE" => Ok(vec!["admin"]), + _ => Err(OauthError::InvalidScope), + }? + .iter() + .map(|s| format!("{}:{}", s, required_scope)) + .collect::>(); + + let valid = authorized_scopes + .split_whitespace() + .any(|s| valid_scopes.contains(&s.to_string())); + + if !valid { + return Err(OauthError::InvalidScope); + } + Ok(()) +} + +fn try_from_jwk(jwk: &Jwk) -> Result { + Ok(match &jwk.algorithm { + AlgorithmParameters::OctetKeyPair(p) => { + let x = base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(&p.x)?; + Ed25519PublicKey::from_bytes(&x)? + } + _ => return Err(OauthError::InvalidJwk), + }) +} + +#[derive(Debug, thiserror::Error)] +pub enum OauthError { + /// Json error + #[error(transparent)] + JsonError(#[from] serde_json::Error), + /// JWT error from jwt-simple crate + #[error(transparent)] + JwtSimpleError(#[from] jwt_simple::Error), + /// Base64 decoding error + #[error(transparent)] + Base64DecodeError(#[from] base64::DecodeError), + /// Invalid JWK + #[error("invalid jwk")] + InvalidJwk, + /// Invalid JWT missing subject + #[error("missing subject")] + InvalidJwtNoSubject, + /// Invalid scope + #[error("invalid scope")] + InvalidScope, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_verify_scope_get() { + assert!(verify_scope("read:self foo bar", "self", "GET").is_ok()); + assert!(verify_scope("write:self foo bar", "self", "GET").is_ok()); + assert!(verify_scope("admin:self foo bar", "self", "GET").is_ok()); + assert!(verify_scope("foo bar", "self", "GET").is_err()); + } + + #[test] + fn should_verify_scope_post() { + assert!(verify_scope("write:self foo bar", "self", "POST").is_ok()); + assert!(verify_scope("admin:self foo bar", "self", "POST").is_ok()); + assert!(verify_scope("read:self foo bar", "self", "POST").is_err()); + assert!(verify_scope("foo bar", "self", "POST").is_err()); + } + + #[test] + fn should_verify_scope_put() { + assert!(verify_scope("write:self foo bar", "self", "PUT").is_ok()); + assert!(verify_scope("admin:self foo bar", "self", "PUT").is_ok()); + assert!(verify_scope("read:self foo bar", "self", "PUT").is_err()); + assert!(verify_scope("foo bar", "self", "PUT").is_err()); + } + + #[test] + fn should_verify_scope_delete() { + assert!(verify_scope("admin:self foo bar", "self", "DELETE").is_ok()); + assert!(verify_scope("write:self foo bar", "self", "DELETE").is_err()); + assert!(verify_scope("read:self foo bar", "self", "DELETE").is_err()); + assert!(verify_scope("foo bar", "self", "DELETE").is_err()); + } + + #[test] + fn should_verify_oauth_token() { + let uid = "842ddbc8-56ec-408d-9fa8-7a8c37ad22a7"; + let key = Ed25519KeyPair::generate(); + let jwk = mk_jwk(key.public_key()); + let token = Claims::with_custom_claims( + OAuthToken { + scope: "write:foo read:test admin:bar".to_string(), + }, + Duration::from_secs(3600), + ) + .with_subject(uid); + let jwt = key.sign::(token).unwrap(); + let subject = + verify_oauth_token(&serde_json::to_string(&jwk).unwrap(), &jwt, "test", "GET").unwrap(); + assert_eq!(&subject, uid); + } + fn mk_jwk(key: Ed25519PublicKey) -> Jwk { + let x = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(&key.to_bytes()); + Jwk { + common: CommonParameters::default(), + algorithm: AlgorithmParameters::OctetKeyPair(OctetKeyPairParameters { + key_type: OctetKeyPairType::OctetKeyPair, + curve: EdwardCurve::Ed25519, + x, + }), + } + } +} diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index bfb65d66bb..2503fa406a 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -179,6 +179,7 @@ instance ToSchema OAuthResponseType where -- in location settings for the endpoint(s) in question -- - Update `charts/nginz/values.yaml` and add `enable_oauth: true` to the endpoint in question -- - Consider writing an integration test +-- todo(leif): update these docs data OAuthScope = ReadFeatureConfigs | ReadSelf @@ -219,8 +220,8 @@ instance (IsOAuthScope scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': s instance ToByteString OAuthScope where builder = \case - WriteConversation -> "write:conversation" - WriteConversationCode -> "write:conversation_code" + WriteConversation -> "write:conversations" + WriteConversationCode -> "write:conversations_code" ReadSelf -> "read:self" ReadFeatureConfigs -> "read:feature_configs" @@ -228,8 +229,8 @@ instance FromByteString OAuthScope where parser = do s <- parser case s & T.toLower of - "write:conversation" -> pure WriteConversation - "write:conversation_code" -> pure WriteConversationCode + "write:conversations" -> pure WriteConversation + "write:conversations_code" -> pure WriteConversationCode "read:self" -> pure ReadSelf "read:feature_configs" -> pure ReadFeatureConfigs _ -> fail "invalid scope" diff --git a/libs/wire-api/src/Wire/API/Routes/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index 0500983e1b..607933e2ed 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -17,21 +17,18 @@ module Wire.API.Routes.API ( API, - hoistAPI, hoistAPIHandler, + hoistAPI, mkAPI, mkNamedAPI, - hoistServerWithDomain, - hoistServerWithDomainAndJwk, (<@>), ServerEffect (..), ServerEffects (..), + hoistServerWithDomain, ) where -import Crypto.JOSE (JWK) import Data.Domain -import Data.Kind (Type) import Data.Proxy import Imports import Polysemy @@ -50,29 +47,15 @@ mkAPI :: (HasServer api '[Domain], ServerEffects (DeclaredErrorEffects api) r0) => ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> API api r0 -mkAPI = mkAPIWithContext @'[Domain] - -mkAPIWithContext :: - forall (context :: [Type]) r0 api. - (HasServer api context, ServerEffects (DeclaredErrorEffects api) r0) => - ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> - API api r0 -mkAPIWithContext h = API $ hoistServerWithContext (Proxy @api) (Proxy @context) (interpretServerEffects @(DeclaredErrorEffects api) @r0) h +mkAPI h = API $ hoistServerWithDomain @api (interpretServerEffects @(DeclaredErrorEffects api) @r0) h -- | Convert a polysemy handler to a named 'API' value. mkNamedAPI :: forall name r0 api. - (HasServer api '[Domain, Maybe JWK], ServerEffects (DeclaredErrorEffects api) r0) => - ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> - API (Named name api) r0 -mkNamedAPI = mkNamedAPIWithContext @'[Domain, Maybe JWK] - -mkNamedAPIWithContext :: - forall (context :: [Type]) name r0 api. - (HasServer api context, ServerEffects (DeclaredErrorEffects api) r0) => + (HasServer api '[Domain], ServerEffects (DeclaredErrorEffects api) r0) => ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> API (Named name api) r0 -mkNamedAPIWithContext = API . Named . unAPI . mkAPIWithContext @context @r0 @api +mkNamedAPI = API . Named . unAPI . mkAPI @r0 @api -- | Combine APIs. (<@>) :: API api1 r -> API api2 r -> API (api1 :<|> api2) r @@ -94,22 +77,13 @@ hoistServerWithDomain :: ServerT api n hoistServerWithDomain = hoistServerWithContext (Proxy @api) (Proxy @'[Domain]) --- | Like `hoistServerWithDomain`, but with a additional 'Maybe JWK' context. -hoistServerWithDomainAndJwk :: - forall api m n. - HasServer api '[Domain, Maybe JWK] => - (forall x. m x -> n x) -> - ServerT api m -> - ServerT api n -hoistServerWithDomainAndJwk = hoistServerWithContext (Proxy @api) (Proxy @'[Domain, Maybe JWK]) - hoistAPIHandler :: forall api r n. - HasServer api '[Domain, Maybe JWK] => + HasServer api '[Domain] => (forall x. Sem r x -> n x) -> API api r -> ServerT api n -hoistAPIHandler f = hoistServerWithContext (Proxy @api) (Proxy @'[Domain, Maybe JWK]) f . unAPI +hoistAPIHandler f = hoistServerWithDomain @api f . unAPI hoistAPI :: forall api1 api2 r1 r2. diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index bdeb4ef5f0..bd2222e736 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -30,42 +30,26 @@ module Wire.API.Routes.Public ZBot, ZConversation, ZProvider, - - -- * OAuth combinators - ZOauthUser, - ZOAuthLocalUser, ) where -import Control.Lens hiding (Context) -import Control.Monad.Except -import Crypto.JWT hiding (Context, params, uri, verify) -import Data.ByteString.Conversion (fromByteString) +import Control.Lens ((<>~)) import Data.Domain -import Data.Either.Combinators import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import Data.Id as Id import Data.Kind import Data.Metrics.Servant import Data.Qualified -import Data.SOP -import Data.String.Conversions (cs) import Data.Swagger -import Data.Typeable (typeRep) import GHC.Base (Symbol) -import GHC.TypeLits (KnownSymbol, symbolVal) -import Imports hiding (All, exp, head) -import Network.Wai +import GHC.TypeLits (KnownSymbol) +import Imports hiding (All, head) import qualified Network.Wai as Wai -import Servant hiding (Handler, JSON, Tagged, addHeader, respond) +import Servant hiding (Handler, JSON, addHeader, respond) import Servant.API.Modifiers import Servant.Server.Internal.Delayed import Servant.Server.Internal.DelayedIO -import Servant.Server.Internal.Router import Servant.Swagger (HasSwagger (toSwagger)) -import Servant.Swagger.Internal.Orphans () -import Wire.API.OAuth -import Wire.API.Routes.Bearer mapRequestArgument :: forall mods a b. @@ -80,7 +64,7 @@ mapRequestArgument f x = (SFalse, STrue) -> (fmap . fmap) f x (SFalse, SFalse) -> fmap f x --- | This type exists for the special 'HasSwagger' and 'HasServer' instances. It +-- This type exists for the special 'HasSwagger' and 'HasServer' instances. It -- shows the "Authorization" header in the swagger docs, but expects the -- "Z-Auth" header in the server. This helps keep the swagger docs usable -- through nginz. @@ -168,7 +152,7 @@ instance IsZType 'ZAuthProvider ctx where instance HasTokenType 'ZAuthProvider where tokenType = Just "provider" -data ZAuthServant (ztype :: ZType) (opts :: [Type]) (scopes :: Maybe [OAuthScope]) +data ZAuthServant (ztype :: ZType) (opts :: [Type]) type InternalAuthDefOpts = '[Servant.Required, Servant.Strict] @@ -178,179 +162,52 @@ type InternalAuth ztype opts = (ZHeader ztype) (ZParam ztype) -type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts 'Nothing - -type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts 'Nothing - -type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts 'Nothing +type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts -type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts 'Nothing +type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts -type ZBot = ZAuthServant 'ZAuthBot InternalAuthDefOpts 'Nothing +type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts -type ZConversation = ZAuthServant 'ZAuthConv InternalAuthDefOpts 'Nothing +type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts -type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts 'Nothing +type ZBot = ZAuthServant 'ZAuthBot InternalAuthDefOpts -type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] 'Nothing +type ZConversation = ZAuthServant 'ZAuthConv InternalAuthDefOpts -type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] 'Nothing +type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts -type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] 'Nothing +type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] -type ZOAuthLocalUser (scopes :: [OAuthScope]) = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts ('Just scopes) +type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] -type ZOauthUser (scopes :: [OAuthScope]) = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scopes) +type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] -instance - (HasSwagger api, IsOAuthScopes scopes, scopes ~ (s ': ss), Typeable ztype) => - HasSwagger (ZAuthServant (ztype :: ZType) _opts ('Just scopes) :> api) - where - toSwagger _ = - toSwagger (Proxy @(ZAuthServant ztype _opts ('Nothing :: Maybe [OAuthScope]) :> api)) - & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "OAuth" secScheme) - & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "OAuth" []] - & addScopeDescription - where - secScheme = - SecurityScheme - { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader), - _securitySchemeDescription = - Just $ - "Must be a token retrieved with an oauth handshake. It must be presented in the form 'Bearer '. \ - \See also info on swagger top-level." - } - - addScopeDescription :: Swagger -> Swagger - addScopeDescription = allOperations . description %~ Just . (<> "\nOAuth scope(s): " <> showOAuthScopeList @scopes) . fold - -instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where +instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) where toSwagger _ = toSwagger (Proxy @api) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "ZAuth" []] - & addZTypeInfo where secScheme = SecurityScheme { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader), - _securitySchemeDescription = - Just $ - "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in the form 'Bearer '. \ - \See also info on swagger top-level." + _securitySchemeDescription = Just "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'." } - addZTypeInfo :: Swagger -> Swagger - addZTypeInfo = - -- Don't use `tokenType @ztype` here, it's `Nothing` for everything but bot, provider! - allOperations . description %~ Just . (<> "\nZAuth token type: " <> (cs . show . typeRep $ (Proxy @ztype))) . fold +instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts :> api) where + toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts :> api)) -instance HasLink endpoint => HasLink (ZAuthServant usr opts scopes :> endpoint) where - type MkLink (ZAuthServant _ _ _ :> endpoint) a = MkLink endpoint a +instance HasLink endpoint => HasLink (ZAuthServant usr opts :> endpoint) where + type MkLink (ZAuthServant _ _ :> endpoint) a = MkLink endpoint a toLink toA _ = toLink toA (Proxy @endpoint) --- | Handle routes that support both ZAuth and OAuth, tried in that order (scopes is Just). --- --- The difference between the two `HasServer` instances for (1) `ZAuthServant ztype opts --- ('Just scopes)` and (2) `ZAuthServant ztype opts Nothing`, resp.: --- --- (1) zauth-or-oauth: enforce required, strict parsing; lookup headers directly, don't --- implement in terms of another servant combinator type. --- --- (2) zauth-only: allow for optional, lenient parsing of `Z-*` headers; implemented in terms --- of `InternalAuth ztype opts`. --- --- Due to these differences, the two instances, or the two functions `finalizeZAuthOrOAuth` --- and `checkZType`, are a bit awkward to consolidate. We just leave the two implementations --- independent for now. instance - ( IsZType ztype ctx, - HasContextEntry ctx (Maybe JWK), - opts ~ InternalAuthDefOpts, - HasServer api ctx, - IsOAuthScopes (scopes :: [OAuthScope]), - ZParam ztype ~ Id a - ) => - HasServer (ZAuthServant ztype opts ('Just scopes) :> api) ctx + {-# OVERLAPPABLE #-} + HasSwagger api => + HasSwagger (ZAuthServant ztype _opts :> api) where - type - ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = - ZQualifiedParam ztype -> ServerT api m - - route :: - Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> - Context ctx -> - Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> - Router env - route _ ctx subserver = - Servant.route - (Proxy @api) - ctx - (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . finalizeZAuthOrOAuth @ztype @scopes @ctx ctx (tokenType @ztype)))) + toSwagger _ = toSwagger (Proxy @api) - hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s - --- | This function has been derived from `checkZType` below. -finalizeZAuthOrOAuth :: - forall ztype scopes ctx a. - ( IsZType ztype ctx, - HasContextEntry ctx (Maybe JWK), - IsOAuthScopes scopes, - ZParam ztype ~ Id a - ) => - Context ctx -> - Maybe ByteString {- FUTUREWORK: use type-level `ztype` instead? does `tokenType @ztype` inside this function have to be incoherent? -} -> - Request -> - DelayedIO (ZParam ztype) -finalizeZAuthOrOAuth ctx mTokenType req = - case lookupHeaders of - -- if the ztype requires a Z-Type header (instance of 'HasTokenType' returns a Just ...), we expect it to match the type we're looking for - (Just expType, Just actType, Just t, Nothing) | expType == actType -> zauth t - -- auth fails if the ztype doesn't match - (Just _, _, _, _) -> delayedFailFatal error403 - -- if the ztype does not require a Z-Type header, we just care for the ZParam ('Z-User' etc.) header - (Nothing, _, Just t, Nothing) -> zauth t - -- if *only* the 'Z-Oauth' header is present, we try to authenticate with OAuth - (Nothing, Nothing, Nothing, Just t) -> oauth t - -- any other case should fail - (Nothing, _, _, _) -> delayedFailFatal error403 - where - lookupHeaders :: (Maybe ByteString, Maybe ByteString, Maybe ByteString, Maybe ByteString) - lookupHeaders = (mTokenType, lookup "Z-Type" hs, lookup headerName hs, lookup "Z-OAuth" hs) - where - hs = requestHeaders req - - headerName :: IsString n => n - headerName = fromString $ symbolVal (Proxy @(ZHeader ztype)) - - zauth :: ByteString -> DelayedIO (ZParam ztype) - zauth = maybe (delayedFailFatal error403) pure . fromByteString @(ZParam ztype) - - oauth :: ByteString -> DelayedIO (ZParam ztype) - oauth = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure - - doOAuth :: Maybe JWK -> ByteString -> DelayedIO (Either ServerError (ZParam ztype)) - doOAuth mJwk h = tryOAuth - where - tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) - tryOAuth = do - let jwkOrError = maybeToRight jwtError mJwk - let tokenOrError = mapLeft invalidOAuthToken $ parseHeader h - either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError - - verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZParam ztype)) - verifyOAuthToken (token, key) = do - verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) - pure $ - verifiedOrError >>= \claimSet -> - if hasScope @scopes claimSet - then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) - else Left insufficientScope - --- | Handle routes that support ZAuth, but not OAuth (scopes is Nothing). --- --- See `HasServer` instance for `ZAuthServant ztype opts ('Just scopes)` for comparison --- (especially if you plan to change the code here). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, @@ -358,56 +215,39 @@ instance SBoolI (FoldRequired opts), HasServer api ctx ) => - HasServer (ZAuthServant ztype opts 'Nothing :> api) ctx + HasServer (ZAuthServant ztype opts :> api) ctx where type - ServerT (ZAuthServant ztype opts 'Nothing :> api) m = + ServerT (ZAuthServant ztype opts :> api) m = RequestArgument opts (ZQualifiedParam ztype) -> ServerT api m - route :: - Proxy (ZAuthServant ztype opts 'Nothing :> api) -> - Context ctx -> - Delayed env (Server (ZAuthServant ztype opts 'Nothing :> api)) -> - Router env route _ ctx subserver = do Servant.route (Proxy @(InternalAuth ztype opts :> api)) ctx ( fmap (. mapRequestArgument @opts (qualifyZParam @ztype ctx)) - (addAuthCheck (fmap const subserver) (withRequest (checkZType (tokenType @ztype)))) + (addAcceptCheck subserver (withRequest (checkType (tokenType @ztype)))) ) + where + checkType :: Maybe ByteString -> Wai.Request -> DelayedIO () + checkType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of + (Just t, value) + | value /= Just t -> + delayedFail + ServerError + { errHTTPCode = 403, + errReasonPhrase = "Access denied", + errBody = "", + errHeaders = [] + } + _ -> pure () + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s -checkZType :: Maybe ByteString -> Wai.Request -> DelayedIO () -checkZType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of - (Just t, value) | value /= Just t -> delayedFail error403 - _ -> pure () - -error403 :: ServerError -error403 = - ServerError - { errHTTPCode = 403, - errReasonPhrase = "Access denied", - errBody = "", - errHeaders = [] - } - -instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts scopes :> api) where +instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts :> api) where getRoutes = getRoutes @api -- FUTUREWORK: Make a PR to the servant-swagger package with this instance instance ToSchema a => ToSchema (Headers ls a) where declareNamedSchema _ = declareNamedSchema (Proxy @a) - --------------------------------------------------------------------------------- --- Util - -insufficientScope :: ServerError -insufficientScope = err403 {errReasonPhrase = "Access denied", errBody = "Insufficient scope"} - -jwtError :: ServerError -jwtError = err500 {errReasonPhrase = "jwt-error", errBody = "Internal error while handling JWT token"} - -invalidOAuthToken :: Text -> ServerError -invalidOAuthToken t = err403 {errReasonPhrase = "Access denied", errBody = "Invalid token: " <> cs t} diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index f445954e49..8771a05ab7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -49,7 +49,6 @@ import Wire.API.Error.Empty import Wire.API.MLS.KeyPackage import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall -import Wire.API.OAuth import Wire.API.Properties import Wire.API.Routes.Bearer import Wire.API.Routes.Cookies @@ -262,7 +261,7 @@ type SelfAPI = Named "get-self" ( Summary "Get your own profile" - :> ZOauthUser '[ 'ReadSelf] + :> ZUser :> "self" :> Get '[JSON] SelfProfile ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 77f5a50424..3f47ba4f19 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -33,7 +33,6 @@ import Wire.API.Event.Conversation import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall -import Wire.API.OAuth import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -339,7 +338,7 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZOAuthLocalUser '[ 'WriteConversation] + :> ZLocalUser :> ZOptConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv @@ -358,7 +357,7 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZOAuthLocalUser '[ 'WriteConversation] + :> ZLocalUser :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv @@ -586,7 +585,7 @@ type ConversationAPI = :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'GuestLinksDisabled - :> ZOauthUser '[ 'WriteConversationCode] + :> ZUser :> ZOptConn :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 8f8bd140d9..a07a09fdfb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -26,7 +26,6 @@ import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MakesFederatedCall -import Wire.API.OAuth import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -215,7 +214,7 @@ type AllFeatureConfigsUserGet = :> Description "Gets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.\ \If the user is not a member of a team, this will return the personal feature configs (the server defaults)." - :> ZOauthUser '[ 'ReadFeatureConfigs] + :> ZUser :> CanThrow 'NotATeamMember :> CanThrow OperationDenied :> CanThrow 'TeamNotFound diff --git a/nix/pkgs/zauth/default.nix b/nix/pkgs/zauth/default.nix index ef1248b73c..5049370ad6 100644 --- a/nix/pkgs/zauth/default.nix +++ b/nix/pkgs/zauth/default.nix @@ -16,7 +16,7 @@ rustPlatform.buildRustPackage rec { src = nix-gitignore.gitignoreSourcePure [ ../../../.gitignore ] ../../../libs/libzauth; sourceRoot = "libzauth/libzauth-c"; - cargoSha256 = "sha256-od+O5dhAVC1KhDUz8U2fhjyqjXkqHjeEEhvVE0N9orI="; + cargoSha256 = "sha256-WVtCbmKHsBsNAeSHPxQyAmWVBpQFGthCKC2hJxmcMYI="; patchLibs = lib.optionalString stdenv.isDarwin '' install_name_tool -id $out/lib/libzauth.dylib $out/lib/libzauth.dylib diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index eeca275079..3a2be7f91e 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -195,7 +195,7 @@ optSettings: setEnableMLS: true setOAuthJwkKeyPair: test/resources/oauth/ed25519.jwk setOAuthAuthCodeExpirationTimeSecs: 3 # 3 secs - setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks + setOAuthAccessTokenExpirationTimeSecs: 3 # 3 secs setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index c691e95656..b4035c3beb 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -48,7 +48,6 @@ import Control.Exception.Safe (catchAny) import Control.Lens (view, (.~), (^.)) import Control.Monad.Catch (MonadCatch, finally) import Control.Monad.Random (randomRIO) -import Crypto.JWT import qualified Data.Aeson as Aeson import Data.Default (Default (def)) import Data.Id (RequestId (..)) @@ -68,7 +67,7 @@ import Network.Wai.Routing.Route (App) import Network.Wai.Utilities (lookupRequestId) import Network.Wai.Utilities.Server import qualified Network.Wai.Utilities.Server as Server -import Polysemy (Members) +import Polysemy (Member) import Servant (Context ((:.)), (:<|>) (..)) import qualified Servant import System.Logger (msg, val, (.=), (~~)) @@ -79,7 +78,6 @@ import Wire.API.Routes.API import Wire.API.Routes.Public.Brig import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai -import Wire.Sem.Jwk (readJwk) import qualified Wire.Sem.Paging as P -- FUTUREWORK: If any of these async threads die, we will have no clue about it @@ -121,8 +119,7 @@ run o = do mkApp :: Opts -> IO (Wai.Application, Env) mkApp o = do e <- newEnv o - mJwk <- join <$> forM (setOAuthJwkKeyPair $ view settings e) readJwk - pure (middleware e $ \reqId -> servantApp mJwk (e & requestId .~ reqId), e) + pure (middleware e $ \reqId -> servantApp (e & requestId .~ reqId), e) where rtree :: Tree (App (Handler BrigCanonicalEffects)) rtree = compile sitemap @@ -139,14 +136,14 @@ mkApp o = do app e r k = runHandler e r (Server.route rtree r k) k -- the servant API wraps the one defined using wai-routing - servantApp :: Maybe JWK -> Env -> Wai.Application - servantApp mJwk e = + servantApp :: Env -> Wai.Application + servantApp e = let localDomain = view (settings . federationDomain) e in Servant.serveWithContext (Proxy @ServantCombinedAPI) - (mJwk :. customFormatters :. localDomain :. Servant.EmptyContext) + (customFormatters :. localDomain :. Servant.EmptyContext) ( docsAPI - :<|> hoistServerWithDomainAndJwk @BrigAPI (toServantHandler e) servantSitemap + :<|> hoistServerWithDomain @BrigAPI (toServantHandler e) servantSitemap :<|> hoistServerWithDomain @IAPI.API (toServantHandler e) IAPI.servantSitemap :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> hoistServerWithDomain @VersionAPI (toServantHandler e) versionAPI @@ -188,7 +185,7 @@ bodyParserErrorFormatter _ _ errMsg = Servant.errHeaders = [(HTTP.hContentType, HTTPMedia.renderHeader (Servant.contentType (Proxy @Servant.JSON)))] } -pendingActivationCleanup :: forall r p. (P.Paging p, Members '[UserPendingActivationStore p] r) => AppT r () +pendingActivationCleanup :: forall r p. (P.Paging p, Member (UserPendingActivationStore p) r) => AppT r () pendingActivationCleanup = do safeForever "pendingActivationCleanup" $ do now <- liftIO =<< view currentTime diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 51f333a044..d52bdd9272 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -59,12 +59,12 @@ import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolProteusTag)) import qualified Wire.API.Conversation.Role as Role import Wire.API.OAuth import Wire.API.Routes.Bearer (Bearer (Bearer, unBearer)) -import Wire.API.User (SelfProfile, User (userId), userEmail) +import Wire.API.User (User (userId), userEmail) import Wire.API.User.Auth (CookieType (PersistentCookie)) import Wire.Sem.Jwk (readJwk) -tests :: Manager -> C.ClientState -> Brig -> Galley -> Nginz -> Opts -> TestTree -tests m db b g n o = do +tests :: Manager -> C.ClientState -> Brig -> Nginz -> Opts -> TestTree +tests m db b n o = do testGroup "oauth" [ test m "register new oauth client" $ testRegisterNewOAuthClient b, @@ -94,32 +94,22 @@ tests m db b g n o = do ], testGroup "accessing a resource" - [ test m "success (internal)" $ testAccessResourceSuccessInternal b, - test m "success (nginz)" $ testAccessResourceSuccessNginz b n, - test m "insufficient scope" $ testAccessResourceInsufficientScope b, - test m "expired token" $ testAccessResourceExpiredToken o b, - test m "nonsense token" $ testAccessResourceNonsenseToken b, - test m "no token" $ testAccessResourceNoToken b, - test m "invalid signature" $ testAccessResourceInvalidSignature o b + [ test m "success (nginz)" $ testAccessResourceSuccessNginz b n, + test m "insufficient scope" $ testAccessResourceInsufficientScope b n, + test m "expired token" $ testAccessResourceExpiredToken b n, + test m "nonsense token" $ testAccessResourceNonsenseToken n, + test m "no token" $ testAccessResourceNoToken n, + test m "invalid signature" $ testAccessResourceInvalidSignature o b n ], testGroup "accessing resources" - [ testGroup - "internal" - [ test m "write:conversation" $ testWriteConversationSuccessInternal b g, - test m "read:feature_configs" $ testReadFeatureConfigsSuccessInternal b g, - test m "write:conversation_code" $ testWriteConversationCodeSuccessInternal b g - ], - testGroup - "nginz" - [ test m "write:conversation" $ testWriteConversationSuccessNginz b n, - test m "read:feature_configs" $ testReadFeatureConfigsSuccessNginz b n, - test m "write:conversation_code" $ testWriteConversationCodeSuccessNginz b n - ] + [ test m "write:conversation" $ testWriteConversationSuccessNginz b n, + test m "read:feature_configs" $ testReadFeatureConfigsSuccessNginz b n, + test m "write:conversation_code" $ testWriteConversationCodeSuccessNginz b n ], testGroup "refresh tokens" $ [ test m "max active tokens" $ testRefreshTokenMaxActiveTokens o db b, - test m "refresh access token - success" $ testRefreshTokenRetrieveAccessToken o b, + test m "refresh access token - success" $ testRefreshTokenRetrieveAccessToken b n, test m "wrong signature - fail" $ testRefreshTokenWrongSignature o b, test m "no token id - fail" $ testRefreshTokenNoTokenId o b, test m "non-existing id - fail" $ testRefreshTokenNonExistingId o b, @@ -341,20 +331,6 @@ assertAccessDenied = do const 403 === statusCode const (Just "forbidden") === fmap Error.label . responseJsonMaybe -testAccessResourceSuccessInternal :: Brig -> Http () -testAccessResourceSuccessInternal brig = do - uid <- userId <$> createUser "alice" brig - let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - accessToken <- createOAuthAccessToken brig accessTokenRequest - -- should succeed with Z-User header - response :: SelfProfile <- responseJsonError =<< get (brig . paths ["self"] . zUser uid) Nginz -> Http () testAccessResourceSuccessNginz brig nginz = do -- with ZAuth header @@ -371,49 +347,45 @@ testAccessResourceSuccessNginz brig nginz = do oauthToken <- oatAccessToken <$> createOAuthAccessToken brig accessTokenRequest get (nginz . paths ["self"] . authHeader oauthToken) !!! const 200 === statusCode -testAccessResourceInsufficientScope :: Brig -> Http () -testAccessResourceInsufficientScope brig = do +testAccessResourceInsufficientScope :: Brig -> Nginz -> Http () +testAccessResourceInsufficientScope brig nginz = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversation] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest - get (brig . paths ["self"] . zOAuthHeader (oatAccessToken accessToken)) !!! do - const 403 === statusCode - const "Access denied" === statusMessage - const (Just "Insufficient scope") === responseBody - -testAccessResourceExpiredToken :: Opt.Opts -> Brig -> Http () -testAccessResourceExpiredToken opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthAccessTokenExpirationTimeSecsInternal ?~ 1) $ do - uid <- userId <$> createUser "alice" brig - let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - accessToken <- createOAuthAccessToken brig accessTokenRequest - liftIO $ threadDelay (1 * 1200 * 1000) - get (brig . paths ["self"] . zOAuthHeader (oatAccessToken accessToken)) !!! do - const 403 === statusCode - const "Access denied" === statusMessage - const (Just "Invalid token: JWTExpired") === responseBody - -testAccessResourceNonsenseToken :: Brig -> Http () -testAccessResourceNonsenseToken brig = do - get (brig . paths ["self"] . zOAuthHeader @Text "foo") !!! do + get (nginz . paths ["self"] . authHeader (oatAccessToken accessToken)) !!! do const 403 === statusCode - const "Access denied" === statusMessage - const (Just "Invalid token: Failed reading: JWSError") =~= responseBody - -testAccessResourceNoToken :: Brig -> Http () -testAccessResourceNoToken brig = - get (brig . paths ["self"]) !!! do - const 403 === statusCode - const "Access denied" === statusMessage + const "Forbidden" === statusMessage -testAccessResourceInvalidSignature :: Opt.Opts -> Brig -> Http () -testAccessResourceInvalidSignature opts brig = do +testAccessResourceExpiredToken :: Brig -> Nginz -> Http () +testAccessResourceExpiredToken brig nginz = do + uid <- userId <$> createUser "alice" brig + let redirectUrl = mkUrl "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ReadSelf] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + accessToken <- createOAuthAccessToken brig accessTokenRequest + liftIO $ threadDelay (5 * 1000 * 1000) + get (nginz . paths ["self"] . authHeader (oatAccessToken accessToken)) !!! do + const 401 === statusCode + const "Unauthorized" === statusMessage + +testAccessResourceNonsenseToken :: Nginz -> Http () +testAccessResourceNonsenseToken nginz = do + get (nginz . paths ["self"] . authHeader @Text "foo") !!! do + const 401 === statusCode + const "Unauthorized" === statusMessage + +testAccessResourceNoToken :: Nginz -> Http () +testAccessResourceNoToken nginz = + get (nginz . paths ["self"]) !!! do + const 401 === statusCode + const "Unauthorized" === statusMessage + +testAccessResourceInvalidSignature :: Opt.Opts -> Brig -> Nginz -> Http () +testAccessResourceInvalidSignature opts brig nginz = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] @@ -423,10 +395,9 @@ testAccessResourceInvalidSignature opts brig = do key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") claimSet <- fromRight (error "token invalid") <$> liftIO (verify key (unOAuthToken $ oatAccessToken accessToken)) tokenSignedWithotherKey <- signAccessToken badKey claimSet - get (brig . paths ["self"] . zOAuthHeader (OAuthToken tokenSignedWithotherKey)) !!! do - const 403 === statusCode - const "Access denied" === statusMessage - const (Just "Invalid token: JWSError JWSInvalidSignature") === responseBody + get (nginz . paths ["self"] . authHeader (OAuthToken tokenSignedWithotherKey)) !!! do + const 401 === statusCode + const "Unauthorized" === statusMessage testRefreshTokenMaxActiveTokens :: Opts -> C.ClientState -> Brig -> Http () testRefreshTokenMaxActiveTokens opts db brig = @@ -486,22 +457,20 @@ testRefreshTokenMaxActiveTokens opts db brig = hasSameElems :: (Eq a) => [a] -> [a] -> Bool hasSameElems x y = null (x \\ y) && null (y \\ x) -testRefreshTokenRetrieveAccessToken :: Opts -> Brig -> Http () -testRefreshTokenRetrieveAccessToken opts brig = - -- overriding settings and set access token to expire in 2 seconds - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthAccessTokenExpirationTimeSecsInternal ?~ 2) $ do - uid <- userId <$> createUser "alice" brig - let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - accessToken <- createOAuthAccessToken brig accessTokenRequest - get (brig . paths ["self"] . zOAuthHeader (oatAccessToken accessToken)) !!! const 200 === statusCode - threadDelay $ 2 * 1000 * 1000 -- wait 2 seconds for access token to expire - get (brig . paths ["self"] . zOAuthHeader (oatAccessToken accessToken)) !!! const 403 === statusCode - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret (oatRefreshToken accessToken) - refreshedToken <- refreshOAuthAccessToken brig refreshAccessTokenRequest - get (brig . paths ["self"] . zOAuthHeader (oatAccessToken refreshedToken)) !!! const 200 === statusCode +testRefreshTokenRetrieveAccessToken :: Brig -> Nginz -> Http () +testRefreshTokenRetrieveAccessToken brig nginz = do + uid <- userId <$> createUser "alice" brig + let redirectUrl = mkUrl "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ReadSelf] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + accessToken <- createOAuthAccessToken brig accessTokenRequest + get (nginz . paths ["self"] . authHeader (oatAccessToken accessToken)) !!! const 200 === statusCode + threadDelay $ 5 * 1000 * 1000 -- wait 5 seconds for access token to expire + get (nginz . paths ["self"] . authHeader (oatAccessToken accessToken)) !!! const 401 === statusCode + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret (oatRefreshToken accessToken) + refreshedToken <- refreshOAuthAccessToken brig refreshAccessTokenRequest + get (nginz . paths ["self"] . authHeader (oatAccessToken refreshedToken)) !!! const 200 === statusCode testRefreshTokenWrongSignature :: Opts -> Brig -> Http () testRefreshTokenWrongSignature opts brig = do @@ -664,11 +633,11 @@ testRevokeApplicationAccountAccess brig = do liftIO $ assertEqual "apps" 0 (length apps) _ -> liftIO $ assertFailure "unexpected number of apps" -testWriteConversationSuccessInternal :: Brig -> Galley -> Http () -testWriteConversationSuccessInternal brig galley = do +testWriteConversationSuccessInternal :: Brig -> Nginz -> Http () +testWriteConversationSuccessInternal brig nginz = do (uid, tid) <- Team.createUserWithTeam brig accessToken <- getAccessTokenForScope brig uid [WriteConversation] - createTeamConv galley zOAuthHeader (oatAccessToken accessToken) tid "oauth test group" !!! do + createTeamConv nginz authHeader (oatAccessToken accessToken) tid "oauth test group" !!! do const 201 === statusCode testWriteConversationSuccessNginz :: Brig -> Nginz -> Http () @@ -678,11 +647,11 @@ testWriteConversationSuccessNginz brig nginz = do createTeamConv nginz authHeader (oatAccessToken accessToken) tid "oauth test group" !!! do const 201 === statusCode -testReadFeatureConfigsSuccessInternal :: Brig -> Galley -> Http () -testReadFeatureConfigsSuccessInternal brig galley = do +testReadFeatureConfigsSuccessInternal :: Brig -> Nginz -> Http () +testReadFeatureConfigsSuccessInternal brig nginz = do (uid, _) <- Team.createUserWithTeam brig accessToken <- getAccessTokenForScope brig uid [ReadFeatureConfigs] - getFeatureConfigs galley zOAuthHeader (oatAccessToken accessToken) !!! do + getFeatureConfigs nginz authHeader (oatAccessToken accessToken) !!! do const 200 === statusCode testReadFeatureConfigsSuccessNginz :: Brig -> Nginz -> Http () @@ -692,15 +661,15 @@ testReadFeatureConfigsSuccessNginz brig nginz = do getFeatureConfigs nginz authHeader (oatAccessToken accessToken) !!! do const 200 === statusCode -testWriteConversationCodeSuccessInternal :: Brig -> Galley -> Http () -testWriteConversationCodeSuccessInternal brig galley = do +testWriteConversationCodeSuccessInternal :: Brig -> Nginz -> Http () +testWriteConversationCodeSuccessInternal brig nginz = do (uid, tid) <- Team.createUserWithTeam brig accessToken <- getAccessTokenForScope brig uid [WriteConversation, WriteConversationCode] conv <- responseJsonError - =<< createTeamConv galley zOAuthHeader (oatAccessToken accessToken) tid "oauth test group" Nginz -> Http () @@ -773,9 +742,6 @@ verifyRefreshToken jwk jwt = authHeader :: ToHttpApiData a => a -> Request -> Request authHeader = bearer "Authorization" -zOAuthHeader :: ToHttpApiData a => a -> Request -> Request -zOAuthHeader = bearer "Z-OAuth" - bearer :: ToHttpApiData a => HeaderName -> a -> Request -> Request bearer name = header name . toHeader . Bearer diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index cb51eefd06..91b2f15e0b 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -160,7 +160,7 @@ runTests iConf brigOpts otherArgs = do let smtp = SMTP.tests mg lg versionApi = API.Version.tests mg brigOpts b mlsApi = MLS.tests mg b brigOpts - oauthAPI = API.OAuth.tests mg db b g n brigOpts + oauthAPI = API.OAuth.tests mg db b n brigOpts withArgs otherArgs . defaultMain $ testGroup diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 58e32a3c10..87f02e7e34 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -46,7 +46,6 @@ settings: mlsPrivateKeyPaths: removal: ed25519: test/resources/ed25519.pem - oauthPublicJwk: test/resources/oauth/ed25519_public.jwk featureFlags: # see #RefConfigOptions in `/docs/reference` sso: disabled-by-default diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 60511985eb..844ca39064 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -36,7 +36,6 @@ module Galley.Options defConcurrentDeletionEvents, defDeleteConvThrottleMillis, defFanoutLimit, - setOauthPublicJwk, JournalOpts (JournalOpts), awsQueueName, awsEndpoint, @@ -117,8 +116,7 @@ data Settings = Settings _setMlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. _setFeatureFlags :: !FeatureFlags, - _setDisabledAPIVersions :: Maybe (Set Version), - _setOauthPublicJwk :: !(Maybe FilePath) + _setDisabledAPIVersions :: Maybe (Set Version) } deriving (Show, Generic) diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 5978492bbc..b528a6c054 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -64,7 +64,6 @@ import Util.Options import Wire.API.Routes.API import qualified Wire.API.Routes.Public.Galley as GalleyAPI import Wire.API.Routes.Version.Wai -import Wire.Sem.Jwk (readJwk) run :: Opts -> IO () run opts = lowerCodensity $ do @@ -90,7 +89,7 @@ mkApp opts = metrics <- lift $ M.metrics env <- lift $ App.createEnv metrics opts lift $ runClient (env ^. cstate) $ versionCheck schemaVersion - mJwk <- lift $ join <$> forM (opts ^. optSettings . setOauthPublicJwk) readJwk + let logger = env ^. App.applog let middlewares = @@ -103,21 +102,20 @@ mkApp opts = Log.info logger $ Log.msg @Text "Galley application finished." Log.flush logger Log.close logger - pure (middlewares $ servantApp env mJwk, env) + pure (middlewares $ servantApp env, env) where rtree = compile API.sitemap runGalley e r k = evalGalleyToIO e (route rtree r k) -- the servant API wraps the one defined using wai-routing - servantApp e0 mJwk r = + servantApp e0 r = let e = reqId .~ lookupReqId r $ e0 in Servant.serveWithContext (Proxy @CombinedAPI) - ( mJwk - :. view (options . optSettings . setFederationDomain) e + ( view (options . optSettings . setFederationDomain) e :. customFormatters :. Servant.EmptyContext ) - ( hoistAPIHandler @GalleyAPI.ServantAPI (toServantHandler e) API.servantSitemap + ( hoistAPIHandler (toServantHandler e) API.servantSitemap :<|> hoistAPIHandler (toServantHandler e) internalAPI :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> Servant.Tagged (runGalley e) diff --git a/services/nginz/integration-test/conf/nginz/common_response_with_zauth.conf b/services/nginz/integration-test/conf/nginz/common_response_with_zauth.conf index ebb5d1d467..97bfb043d8 100644 --- a/services/nginz/integration-test/conf/nginz/common_response_with_zauth.conf +++ b/services/nginz/integration-test/conf/nginz/common_response_with_zauth.conf @@ -1 +1,2 @@ include common_response.conf; + proxy_set_header Authorization ""; diff --git a/services/nginz/integration-test/conf/nginz/common_response_with_zauth_oauth.conf b/services/nginz/integration-test/conf/nginz/common_response_with_zauth_oauth.conf deleted file mode 100644 index aea96ff11a..0000000000 --- a/services/nginz/integration-test/conf/nginz/common_response_with_zauth_oauth.conf +++ /dev/null @@ -1,2 +0,0 @@ - oauth on; - include common_response.conf; diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 974314c6fa..843b8ea103 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -118,6 +118,7 @@ http { zauth_keystore resources/zauth/pubkeys.txt; zauth_acl conf/nginz/zauth_acl.txt; + oauth_key resources/oauth/ed25519_public.jwk; location /status { set $sanitized_request $request; @@ -212,7 +213,8 @@ http { ## brig authenticated endpoints location ~* ^(/v[0-9]+)?/self$ { - include common_response_with_zauth_oauth.conf; + include common_response_with_zauth.conf; + oauth_scope self; proxy_pass http://brig; } @@ -315,8 +317,20 @@ http { proxy_pass http://galley; } + location ~* ^(/v[0-9]+)?/conversations$ { + include common_response_with_zauth.conf; + oauth_scope conversations; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/code { + include common_response_with_zauth.conf; + oauth_scope conversations_code; + proxy_pass http://galley; + } + location ~* ^(/v[0-9]+)?/conversations.* { - include common_response_with_zauth_oauth.conf; + include common_response_with_zauth.conf; proxy_pass http://galley; } @@ -375,8 +389,14 @@ http { proxy_pass http://galley; } + location ~* ^(/v[0-9]+)?/feature-configs$ { + include common_response_with_zauth.conf; + oauth_scope feature_configs; + proxy_pass http://galley; + } + location ~* ^(/v[0-9]+)?/feature-configs(.*) { - include common_response_with_zauth_oauth.conf; + include common_response_with_zauth.conf; proxy_pass http://galley; } diff --git a/services/galley/test/resources/oauth/ed25519_public.jwk b/services/nginz/integration-test/resources/oauth/ed25519_public.jwk similarity index 100% rename from services/galley/test/resources/oauth/ed25519_public.jwk rename to services/nginz/integration-test/resources/oauth/ed25519_public.jwk diff --git a/services/nginz/third_party/nginx-zauth-module/zauth_module.c b/services/nginz/third_party/nginx-zauth-module/zauth_module.c index d2c8068364..6912484196 100644 --- a/services/nginz/third_party/nginx-zauth-module/zauth_module.c +++ b/services/nginz/third_party/nginx-zauth-module/zauth_module.c @@ -9,13 +9,30 @@ typedef struct { ZauthKeystore * keystore; ZauthAcl * acl; + OAuthJwk * oauth_key; } ZauthServerConf; typedef struct { ngx_flag_t zauth; // 1=on, 0=off - ngx_flag_t oauth; // 1=on, 0=off + ngx_str_t oauth_scope; } ZauthLocationConf; +enum { + CONTEXT_NONE = 0, + CONTEXT_ZAUTH, + CONTEXT_OAUTH +}; + +typedef struct { + ngx_int_t tag; + union { + // valid if tag == CONTEXT_ZAUTH + ZauthToken * token; + // valid if tag == CONTEXT_OAUTH + char * user_id; + }; +} ZauthContext; + // Configuration setup static void * create_srv_conf (ngx_conf_t *); static void * create_loc_conf (ngx_conf_t *); @@ -23,21 +40,26 @@ static char * merge_loc_conf (ngx_conf_t *, void *, void *); static char * merge_srv_conf (ngx_conf_t *, void *, void *); static char * load_keystore (ngx_conf_t *, ngx_command_t *, void *); static char * load_acl (ngx_conf_t *, ngx_command_t *, void *); +static char * load_oauth_key (ngx_conf_t *, ngx_command_t *, void *); static void delete_srv_conf (void *); // Module setup -static ngx_int_t zauth_init (ngx_conf_t *); -static ngx_int_t zauth_parse_request (ngx_http_request_t *); +static ngx_int_t zauth_init (ngx_conf_t *); +static ngx_int_t zauth_parse_request (ngx_http_request_t *); static ngx_int_t zauth_and_oauth_handle_request (ngx_http_request_t *); // Request Inspection -static ZauthResult token_from_header (ngx_str_t const *, ZauthToken **); -static ZauthResult token_from_query (ngx_str_t const *, ZauthToken **); -static void delete_token (void *); +static ZauthResult token_from_header (ngx_str_t const *, ZauthToken **); +static ZauthResult token_from_query (ngx_str_t const *, ZauthToken **); +static ZauthContext * alloc_zauth_context (ngx_http_request_t * r, ZauthToken *); +static ZauthContext * alloc_oauth_context (ngx_http_request_t * r, char *); +static ngx_int_t setup_zauth_context (ngx_http_request_t * , ZauthContext *); +static void delete_zauth_context (void *); // Variable manipulation static ngx_int_t zauth_variables (ngx_conf_t *); static ngx_int_t zauth_token_var (ngx_http_request_t *, ngx_http_variable_value_t *, uintptr_t); +static ngx_int_t zauth_token_var_user (ngx_http_request_t *, ngx_http_variable_value_t *, uintptr_t); static ngx_int_t zauth_token_var_conn (ngx_http_request_t *, ngx_http_variable_value_t *, uintptr_t); static ngx_int_t zauth_token_var_conv (ngx_http_request_t *, ngx_http_variable_value_t *, uintptr_t); static ngx_int_t zauth_token_typeinfo (ngx_http_request_t *, ngx_http_variable_value_t *, uintptr_t); @@ -45,10 +67,8 @@ static ngx_int_t zauth_set_var (ngx_pool_t *, ngx_http_variable_value_t * static void zauth_empty_val (ngx_http_variable_value_t *); // Utility functions -static ngx_int_t zauth_handle_zauth_request (ngx_http_request_t *, const ZauthServerConf *); -static ngx_int_t empty_authorization_header_in_headers_in(ngx_http_request_t *); -static ngx_int_t set_custom_header_in_headers_in(ngx_http_request_t *, ngx_str_t *, ngx_str_t *); -static bool zauth_is_authorized_and_allowed(ngx_http_request_t *); +static ngx_int_t zauth_handle_request (ngx_http_request_t *, const ZauthServerConf *, ZauthToken const *); +static ngx_int_t oauth_handle_request(ngx_http_request_t *, OAuthJwk const *, ngx_str_t const); static ngx_http_module_t zauth_module_ctx = { zauth_variables // pre-configuration @@ -70,11 +90,11 @@ static ngx_command_t zauth_commands [] = { , NULL } - , { ngx_string ("oauth") + , { ngx_string ("oauth_scope") , NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1 - , ngx_conf_set_flag_slot + , ngx_conf_set_str_slot , NGX_HTTP_LOC_CONF_OFFSET - , offsetof (ZauthLocationConf, oauth) + , offsetof (ZauthLocationConf, oauth_scope) , NULL } @@ -94,6 +114,14 @@ static ngx_command_t zauth_commands [] = { , NULL } + , { ngx_string ("oauth_key") + , NGX_HTTP_SRV_CONF | NGX_CONF_TAKE1 + , load_oauth_key + , NGX_HTTP_SRV_CONF_OFFSET + , 0 + , NULL + } + , ngx_null_command }; @@ -150,6 +178,10 @@ static char * merge_srv_conf (ngx_conf_t * c, void * pc, void * cc) { child->acl = parent->acl; } + if (child->oauth_key == NULL) { + child->oauth_key = parent->oauth_key; + } + if (child->keystore == NULL) { ngx_conf_log_error(NGX_LOG_EMERG, c, 0, "missing 'zauth_keystore'"); return NGX_CONF_ERROR; @@ -160,6 +192,11 @@ static char * merge_srv_conf (ngx_conf_t * c, void * pc, void * cc) { return NGX_CONF_ERROR; } + if (child->oauth_key == NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, c, 0, "missing 'oauth_key'"); + return NGX_CONF_ERROR; + } + return NGX_CONF_OK; } @@ -171,6 +208,9 @@ static void delete_srv_conf (void * data) { if (c->acl != NULL) { zauth_acl_delete(c->acl); } + if (c->oauth_key != NULL) { + oauth_key_delete(c->oauth_key); + } } static void * create_loc_conf (ngx_conf_t * conf) { @@ -182,7 +222,6 @@ static void * create_loc_conf (ngx_conf_t * conf) { } lc->zauth = NGX_CONF_UNSET; - lc->oauth = NGX_CONF_UNSET; return lc; } @@ -191,7 +230,7 @@ static char * merge_loc_conf (ngx_conf_t * _, void * pc, void * cc) { ZauthLocationConf * parent = pc; ZauthLocationConf * child = cc; ngx_conf_merge_off_value(child->zauth, parent->zauth, 1); - ngx_conf_merge_off_value(child->oauth, parent->oauth, 0); + ngx_conf_merge_str_value(child->oauth_scope, parent->oauth_scope, NULL); return NGX_CONF_OK; } @@ -229,6 +268,23 @@ static char * load_acl (ngx_conf_t * conf, ngx_command_t * cmd, void * data) { return NGX_CONF_OK; } +static char * load_oauth_key (ngx_conf_t * conf, ngx_command_t * cmd, void * data) { + ZauthServerConf * sc = data; + if (sc == NULL) { + return NGX_CONF_ERROR; + } + + ngx_str_t * const fname = conf->args->elts; + OAuthResultStatus status = oauth_key_open(fname[1].data, fname[1].len, &sc->oauth_key); + + if (status != OAUTH_OK || sc->oauth_key == NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, conf, 0, "failed to load oauth key [%d]", status); + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + // Module setup ///////////////////////////////////////////////////////////// static ngx_int_t zauth_init (ngx_conf_t * conf) { @@ -276,77 +332,28 @@ static ngx_int_t zauth_and_oauth_handle_request (ngx_http_request_t * r) { return NGX_DECLINED; } - // let's try to handle zauth - ngx_int_t status = zauth_handle_zauth_request(r, sc); - - // if parsing the token fails, - // and oauth is enabled, - // we try to set the Z-OAuth header, - // and empty the Authorization header - if (status != NGX_OK && status != NGX_HTTP_FORBIDDEN && lc->oauth == 1) { - if (r->headers_in.authorization == NULL) { - return NGX_ERROR; - } - ngx_str_t hdr = r->headers_in.authorization->value; - if (strncmp((char const *) hdr.data, "Bearer ", 7) != 0) { - return NGX_ERROR; - } - ngx_str_t z_oauth_hdr_name = ngx_string("Z-OAuth"); - ngx_int_t res = set_custom_header_in_headers_in(r, &z_oauth_hdr_name, &hdr); - if (res != NGX_OK) { - return NGX_ERROR; - } - res = empty_authorization_header_in_headers_in(r); - if (res != NGX_OK) { - return NGX_ERROR; - } - return NGX_DECLINED; - } - // if zauth succeeds, we empty the Authorization header - else if (status == NGX_OK) { - return empty_authorization_header_in_headers_in(r); - } - // in all other cases (which should only be errors) we return the status - else { - return status; - } -} - -ngx_int_t empty_authorization_header_in_headers_in(ngx_http_request_t *r) { - ngx_table_elt_t * h = r->headers_in.authorization; - if (h == NULL) { - return NGX_OK; - } - ngx_str_t value = ngx_string(""); - h->value = value; - h->hash = 1; - return NGX_OK; -} - -ngx_int_t set_custom_header_in_headers_in(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) { - ngx_table_elt_t *h = ngx_list_push(&r->headers_in.headers); - if (h == NULL) { - return NGX_ERROR; - } - h->key = *key; - h->value = *value; - h->hash = 1; - return NGX_OK; -} - -static ngx_int_t zauth_handle_zauth_request (ngx_http_request_t * r, const ZauthServerConf * sc) { - ZauthToken const * tkn = ngx_http_get_module_ctx(r, zauth_module); + ZauthContext const * ctx = ngx_http_get_module_ctx(r, zauth_module); // internal redirects clear module contexts => try to parse again - if (tkn == NULL && r->internal) { + if (ctx == NULL && r->internal) { ngx_int_t status = zauth_parse_request(r); if (status != NGX_OK) { return status; } else { - tkn = ngx_http_get_module_ctx(r, zauth_module); + ctx = ngx_http_get_module_ctx(r, zauth_module); } } + if (ctx != NULL && ctx->tag == CONTEXT_ZAUTH) { + return zauth_handle_request(r, sc, ctx->token); + } else if (ctx == NULL) { + return oauth_handle_request(r, sc->oauth_key, lc->oauth_scope); + } else { + return NGX_HTTP_UNAUTHORIZED; + } +} + +static ngx_int_t zauth_handle_request (ngx_http_request_t * r, const ZauthServerConf * sc, ZauthToken const * tkn) { if (tkn == NULL) { return NGX_HTTP_UNAUTHORIZED; } @@ -369,8 +376,80 @@ static ngx_int_t zauth_handle_zauth_request (ngx_http_request_t * r, const Zauth return NGX_OK; } -static void delete_token (void * data) { - zauth_token_delete((ZauthToken *) data); +ngx_int_t oauth_handle_request(ngx_http_request_t *r, OAuthJwk const * key, ngx_str_t const scope) { + if (r->headers_in.authorization == NULL) { + return NGX_HTTP_UNAUTHORIZED; + } + + ngx_str_t hdr = r->headers_in.authorization->value; + + if (strncmp((char const *) hdr.data, "Bearer ", 7) == 0) { + OAuthResult res = oauth_verify_token(key, &hdr.data[7], hdr.len - 7, scope.data, scope.len, r->method_name.data, r->method_name.len); + if (res.status == OAUTH_OK) { + ZauthContext * ctx = alloc_oauth_context(r, res.uid); + if (ctx == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR; // for OOM-safety + ngx_int_t e = setup_zauth_context(r, ctx); + if (e != NGX_OK) { + ngx_free(ctx); + return e; + } + + return NGX_OK; + } else if (res.status == OAUTH_INSUFFICIENT_SCOPE) { + ngx_log_error(NGX_LOG_NOTICE, r->connection->log, 0, "OAuth insufficient scope"); + return NGX_HTTP_FORBIDDEN; + } else { + ngx_log_error(NGX_LOG_NOTICE, r->connection->log, 0, "OAuth token verification failed with: %d", res.status); + return NGX_HTTP_UNAUTHORIZED; + } + } else { + return NGX_HTTP_UNAUTHORIZED; + } +} + +static ngx_int_t setup_zauth_context(ngx_http_request_t * r, ZauthContext * ctx) { + ngx_pool_cleanup_t * finaliser = ngx_pool_cleanup_add(r->pool, 0); + if (finaliser == NULL) { + return NGX_ERROR; + } + + finaliser->handler = delete_zauth_context; + finaliser->data = ctx; + ngx_http_set_ctx(r, ctx, zauth_module); + + return NGX_OK; +} + +static ZauthContext * alloc_zauth_context(ngx_http_request_t * r, ZauthToken * token) { + ZauthContext * ctx = ngx_alloc(sizeof(ZauthContext), r->connection->log); + if (ctx == NULL) { + return ctx; + } + ctx->tag = CONTEXT_ZAUTH; + ctx->token = token; + return ctx; +} + + +static ZauthContext * alloc_oauth_context(ngx_http_request_t * r, char * user_id) { + ZauthContext * ctx = ngx_alloc(sizeof(ZauthContext), r->connection->log); + if (ctx == NULL) { + return ctx; + } + ctx->tag = CONTEXT_OAUTH; + ctx->user_id = user_id; + return ctx; +} + +static void delete_zauth_context(void * data) { + ZauthContext *ctx = data; + if (ctx->tag == CONTEXT_ZAUTH) { + zauth_token_delete(ctx->token); + } + else if (ctx->tag == CONTEXT_OAUTH) { + oauth_result_uid_delete(ctx->user_id); + } + ngx_free(ctx); } static ngx_int_t zauth_parse_request (ngx_http_request_t * r) { @@ -400,32 +479,19 @@ static ngx_int_t zauth_parse_request (ngx_http_request_t * r) { } if (res == ZAUTH_OK && tkn != NULL) { - ngx_pool_cleanup_t * finaliser = ngx_pool_cleanup_add(r->pool, 0); - if (finaliser == NULL) { - return NGX_ERROR; + ZauthContext * ctx = alloc_zauth_context(r, tkn); + if (ctx == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR; // for OOM-safety + ngx_int_t e = setup_zauth_context(r, ctx); + if (e != NGX_OK) { + ngx_free(ctx); + return e; } - finaliser->handler = delete_token; - finaliser->data = tkn; - ngx_http_set_ctx(r, tkn, zauth_module); return NGX_OK; } - if (res != ZAUTH_OK) { - ZauthLocationConf const *lc = ngx_http_get_module_loc_conf(r, zauth_module); - - bool is_zauth_parse_error () { - return - lc == NULL || - lc->oauth == 0 || - r->headers_in.authorization == NULL || - strncmp((char const *)r->headers_in.authorization->value.data, "Bearer ", 7) == 0; - } - // only if parsing the request failed we produce a log entry (otherwise the request will be handled by wire-server as an oauth request) - if (is_zauth_parse_error()) - { - ngx_log_error(NGX_LOG_NOTICE, r->connection->log, 0, "failed to parse token [%d]", res); - } + if (res != ZAUTH_OK) { + ngx_log_error(NGX_LOG_NOTICE, r->connection->log, 0, "failed to parse token [%d]", res); } return NGX_OK; @@ -497,7 +563,7 @@ static ngx_int_t zauth_variables (ngx_conf_t * conf) { z_type_var->get_handler = zauth_token_typeinfo; z_bot_var->get_handler = zauth_token_var; z_bot_var->data = 'b'; - z_user_var->get_handler = zauth_token_var; + z_user_var->get_handler = zauth_token_var_user; z_user_var->data = 'u'; z_client_var->get_handler = zauth_token_var; z_client_var->data = 'i'; @@ -510,11 +576,14 @@ static ngx_int_t zauth_variables (ngx_conf_t * conf) { } static ngx_int_t zauth_token_typeinfo (ngx_http_request_t * r, ngx_http_variable_value_t * v, uintptr_t _) { - ZauthToken const * t = ngx_http_get_module_ctx(r, zauth_module); - if (t == NULL) { + ZauthContext const * ctx = ngx_http_get_module_ctx(r, zauth_module); + if (ctx == NULL) { return NGX_ERROR; } - switch (zauth_token_type(t)) { + if (ctx->tag != CONTEXT_ZAUTH) { + return NGX_OK; + } + switch (zauth_token_type(ctx->token)) { case ZAUTH_TOKEN_TYPE_BOT: { Range range = { (u_char*) "bot", 3 }; return zauth_set_var(r->pool, v, range); @@ -546,64 +615,74 @@ static ngx_int_t zauth_token_typeinfo (ngx_http_request_t * r, ngx_http_variable } } -// check if the signature has been validated (authorized) -// and endpoint is either not denied or allowed according to the access control list (acl) configuration -static bool zauth_is_authorized_and_allowed(ngx_http_request_t * r) { - ZauthToken const * t = ngx_http_get_module_ctx(r, zauth_module); +static ngx_int_t zauth_token_var (ngx_http_request_t * r, ngx_http_variable_value_t * v, uintptr_t data) { + ZauthContext const * ctx = ngx_http_get_module_ctx(r, zauth_module); - if (t == NULL) { - return false; - } - - if (zauth_token_verification(t) != ZAUTH_TOKEN_VERIFICATION_SUCCESS) { - return false; - } + // this function checks if the signature has been validated successfully, + // and if access is allowed (endpoint is either allowed or not denied) according to the access control list (ACL) configuration + bool zauth_is_authorized_and_allowed() { + if (ctx == NULL || ctx->tag != CONTEXT_ZAUTH) { + return false; + } - ZauthServerConf const * sc = - ngx_http_get_module_srv_conf(r, zauth_module); + ZauthToken const * t = ctx->token; - if (sc == NULL || sc->acl == NULL) { - return false; - } + if (t == NULL) { + return false; + } + + if (zauth_token_verification(t) != ZAUTH_TOKEN_VERIFICATION_SUCCESS) { + return false; + } - uint8_t is_allowed = 0; - - ngx_int_t res = zauth_token_allowed(t, sc->acl, r->uri.data, r->uri.len, &is_allowed); - - if (res != NGX_OK) { - return false; - } + ZauthServerConf const * sc = + ngx_http_get_module_srv_conf(r, zauth_module); - return is_allowed == 1; -} + if (sc == NULL || sc->acl == NULL) { + return false; + } -static ngx_int_t zauth_token_var (ngx_http_request_t * r, ngx_http_variable_value_t * v, uintptr_t data) { - ZauthToken const * t = ngx_http_get_module_ctx(r, zauth_module); - if (t == NULL) { - return NGX_ERROR; + uint8_t is_allowed = 0; + + ngx_int_t res = zauth_token_allowed(t, sc->acl, r->uri.data, r->uri.len, &is_allowed); + + if (res != NGX_OK) { + return false; + } + + return is_allowed == 1; + } + + // in this function client, provider, and bot ID is retrieved from the ZAuth token + // and assigned to variables that are used in the nginx config to set the corresponding headers (e.g. Z-Client, Z-Provider, ...). + // therefore we want to make sure that the token is authorized (has a valid signature) + // and access is allowed (endpoint is either allowed or not denied) according to the access control list (ACL) configuration + // before we set the variable + // otherwise 'zauth_token_lookup' will crash for OAuth requests + if (ctx != NULL && ctx->tag == CONTEXT_ZAUTH && zauth_is_authorized_and_allowed()) { + return zauth_set_var(r->pool, v, zauth_token_lookup(ctx->token, data)); + } else { + zauth_empty_val(v); + return NGX_OK; } +} - // in this function the user, client, provider, and bot ID is retrieved from the token - // and assigned to variables that are used in the nginx config to set the headers. - // therefore we want to make sure that the token is - // authorized (has a valid signature) - // and access is allowed (endpoint is either not denied or allowed according to the access control list configuration) - // before we set these variables. - if (!zauth_is_authorized_and_allowed(r)) { +static ngx_int_t zauth_token_var_user (ngx_http_request_t * r, ngx_http_variable_value_t * v, uintptr_t _) { + ZauthContext const * ctx = ngx_http_get_module_ctx(r, zauth_module); + if (ctx != NULL && ctx->tag == CONTEXT_ZAUTH) { + return zauth_set_var(r->pool, v, zauth_token_lookup(ctx->token, 'u')); + } else if (ctx != NULL && ctx->tag == CONTEXT_OAUTH) { + return zauth_set_var(r->pool, v, (Range) { (u_char*) ctx->user_id, strlen(ctx->user_id) }); + } else { zauth_empty_val(v); return NGX_OK; } - - return zauth_set_var(r->pool, v, zauth_token_lookup(t, data)); } static ngx_int_t zauth_token_var_conn (ngx_http_request_t * r, ngx_http_variable_value_t * v, uintptr_t _) { - ZauthToken const * t = ngx_http_get_module_ctx(r, zauth_module); - if (t == NULL) { - return NGX_ERROR; - } - if (zauth_token_type(t) == ZAUTH_TOKEN_TYPE_ACCESS || zauth_token_type(t) == ZAUTH_TOKEN_TYPE_LEGAL_HOLD_ACCESS) { - return zauth_set_var(r->pool, v, zauth_token_lookup(t, 'c')); + ZauthContext const * ctx = ngx_http_get_module_ctx(r, zauth_module); + if (ctx != NULL && ctx->tag == CONTEXT_ZAUTH && (zauth_token_type(ctx->token) == ZAUTH_TOKEN_TYPE_ACCESS || zauth_token_type(ctx->token) == ZAUTH_TOKEN_TYPE_LEGAL_HOLD_ACCESS)) { + return zauth_set_var(r->pool, v, zauth_token_lookup(ctx->token, 'c')); } else { zauth_empty_val(v); return NGX_OK; @@ -611,12 +690,9 @@ static ngx_int_t zauth_token_var_conn (ngx_http_request_t * r, ngx_http_variable } static ngx_int_t zauth_token_var_conv (ngx_http_request_t * r, ngx_http_variable_value_t * v, uintptr_t _) { - ZauthToken const * t = ngx_http_get_module_ctx(r, zauth_module); - if (t == NULL) { - return NGX_ERROR; - } - if (zauth_token_type(t) == ZAUTH_TOKEN_TYPE_BOT) { - return zauth_set_var(r->pool, v, zauth_token_lookup(t, 'c')); + ZauthContext const * ctx = ngx_http_get_module_ctx(r, zauth_module); + if (ctx != NULL && ctx->tag == CONTEXT_ZAUTH && zauth_token_type(ctx->token) == ZAUTH_TOKEN_TYPE_BOT) { + return zauth_set_var(r->pool, v, zauth_token_lookup(ctx->token, 'c')); } else { zauth_empty_val(v); return NGX_OK;