Skip to content

Commit

Permalink
feat(rocket): add auth guard for rocket oauth2.0 introspection (#319)
Browse files Browse the repository at this point in the history
This adds rocket as a possible feature to have a
OAuth2.0 Token introspection route guard.
The guard does check the incoming token
and introspects it against a given ZITADEL
instance. A new feature "rocket" is used
to enable all rocket specific stuff.

BREAKING CHANGE: This removes `api` as a default
feature. To migrate, just add `api` to the used features
of the crate.
  • Loading branch information
buehler committed Jan 1, 2023
1 parent 2883a54 commit ea04fd6
Show file tree
Hide file tree
Showing 23 changed files with 1,183 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Expand Up @@ -14,6 +14,8 @@ jobs:
with:
submodules: true
- uses: arduino/setup-protoc@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
Expand All @@ -32,6 +34,8 @@ jobs:
with:
submodules: true
- uses: arduino/setup-protoc@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/testing.yml
Expand Up @@ -14,6 +14,8 @@ jobs:
with:
submodules: true
- uses: arduino/setup-protoc@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- run: rustup component add clippy
- uses: actions-rs/clippy-check@v1
with:
Expand Down
20 changes: 20 additions & 0 deletions .kreya/ZITADEL Rust.krproj
@@ -0,0 +1,20 @@
{
"id": "31a70203-3772-4e6a-8cf2-df399183a921",
"importStreams": [],
"authConfigs": [
{
"name": "ZITADEL",
"providerName": "oidc",
"id": "483e44fe-3c97-4e75-9ec8-0237764b6a3b",
"options": {
"redirectUri": "http://localhost/",
"clientId": "194289529828475137@zitadel_rust_test",
"scope": "openid profile email",
"issuer": "https://zitadel-libraries-l8boqa.zitadel.cloud",
"clientAuthMethod": "none",
"grantType": "authorizationCode"
}
}
],
"certificates": []
}
12 changes: 12 additions & 0 deletions .kreya/directory.krpref
@@ -0,0 +1,12 @@
{
"settings": [
{
"options": {
"rest": {
"endpoint": "http://127.0.0.1:8000/",
"disableServerCertificateValidation": true
}
}
}
]
}
16 changes: 16 additions & 0 deletions .kreya/rocket/Authorized Request.krop
@@ -0,0 +1,16 @@
{
"details": {
"path": "/authed",
"method": "GET",
"headers": []
},
"requests": [
{
"contentType": "none"
}
],
"authId": "483e44fe-3c97-4e75-9ec8-0237764b6a3b",
"operationType": "unary",
"invokerName": "rest",
"typeHint": "GET"
}
15 changes: 15 additions & 0 deletions .kreya/rocket/Unauthorized Request.krop
@@ -0,0 +1,15 @@
{
"details": {
"path": "/unauthed",
"method": "GET",
"headers": []
},
"requests": [
{
"contentType": "none"
}
],
"operationType": "unary",
"invokerName": "rest",
"typeHint": "GET"
}
35 changes: 27 additions & 8 deletions Cargo.toml
Expand Up @@ -21,17 +21,11 @@ include = [
]

[features]
default = ["api"]
default = []

## The API feature enables the gRPC service clients to access the ZITADEL API.
api = ["dep:prost", "dep:prost-types", "dep:tonic", "dep:tonic-types"]

## The interceptors feature enables specific gRPC interceptors and
## new convenience functions to create a gRPC client with interceptors.
## The interceptors provide easy access to an authenticated ZITADEL API client.
## The interceptors work with the credentials from this crate.
interceptors = ["api", "credentials", "dep:time", "dep:tokio"]

## The credentials feature enables special credentials helpers for ZITADEL.
## For example, it allows the user to create a ZITADEL service account and
## authenticate against ZITADEL.
Expand All @@ -42,17 +36,42 @@ credentials = [
"dep:serde",
"dep:serde_json",
"dep:serde_urlencoded",
"dep:time"
"dep:time",
]

## The interceptors feature enables specific gRPC interceptors and
## new convenience functions to create a gRPC client with interceptors.
## The interceptors provide easy access to an authenticated ZITADEL API client.
## The interceptors work with the credentials from this crate.
interceptors = ["api", "credentials", "dep:time", "dep:tokio"]

## The OIDC module enables basic OIDC (OpenID Connect) features to communicate
## with ZITADEL. Two examples are the `discover` and `introspect` functions.
## The OIDC features are required for some of the web framework features.
oidc = [
"credentials",
"dep:base64",
]

## Feature that enables support for the [rocket framework](https://rocket.rs/).
## It enables authentication features for rocket in the form of route guards.
## Refer to the rocket module for more information.
rocket = [
"credentials",
"oidc",
"dep:rocket",
]

[dependencies]
base64 = { version = "0.20.0", optional = true }
custom_error = "1.9.2"
document-features = { version = "0.2", optional = true }
jsonwebtoken = { version = "8.2.0", optional = true }
openidconnect = { version = "2.4.0", optional = true }
prost = { version = "0.11", optional = true }
prost-types = { version = "0.11", optional = true }
reqwest = { version = "0.11.13", features = ["json"], optional = true }
rocket = { version = "0.5.0-rc.2", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
serde_urlencoded = { version = "0.7.1", optional = true }
Expand Down
30 changes: 30 additions & 0 deletions examples/rocket_webapi_oauth_interception_basic.rs
@@ -0,0 +1,30 @@
use zitadel::rocket::introspection::{IntrospectedUser, IntrospectionConfigBuilder};

#[rocket::get("/unauthed")]
fn unauthed() -> &'static str {
"Hello Unauthorized User"
}

#[rocket::get("/authed")]
fn authed(user: &IntrospectedUser) -> String {
format!(
"Hello Authorized {:?} with id {}",
user.username, user.user_id
)
}

#[rocket::launch]
async fn rocket() -> _ {
rocket::build()
.mount("/", rocket::routes![unauthed, authed])
.manage(
IntrospectionConfigBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud")
.with_basic_auth(
"194339055499018497@zitadel_rust_test",
"Ip56oGzxKL1rJ8JaleUVKL7qUlpZ1tqHQYRSd6JE1mTlTJ3pDkDzoObHdZsOg88B",
)
.build()
.await
.unwrap(),
)
}
39 changes: 39 additions & 0 deletions examples/rocket_webapi_oauth_interception_jwtprofile.rs
@@ -0,0 +1,39 @@
use zitadel::{
credentials::Application,
rocket::introspection::{IntrospectedUser, IntrospectionConfigBuilder},
};

const APPLICATION: &str = r#"
{
"type": "application",
"keyId": "181963758610940161",
"key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAwT2YZJytkkZ1DDM3dcu1OA8YPzHu6XR8HotdMNRnV75GhOT4\nB7zDtdtoP8w/1NHHPEJ859e0kYhrrnKikOKLS6fS1KRsmqR5ZvTq8SlZ2mq3RcX2\nebZx5dQt36INij/WXdsBmjM/yfWvqqWBSb0L/186DaWwmmIxoXWe873vxRmlzblg\nGd8Nu07s9YTREbGPbtFVHEUM6xI4oIe8HJ0e1+JBkiGqk31Cogo0FoAxrOAg0Sf4\n5XiUMYIjzqh8673F9SC4IpVxG22mpFk3vDFuAITaStWYbiH2hPJNKWyX9HDCZb1D\nDqa3wZBDiLqWxh22hNZ6ZIe+3UoSGWsPBH+E1wIDAQABAoIBAD2v5QsRPRN57HmF\njAnNir8nimz6CrN53Pl/MbOZypenBSn9UfReXPeb3+6lzCarBPgGnYsBQAJJU16v\n95daym7PVy1Mg+Ll6F9mhe2Qbr+b23+pj2IRTNC6aB6Aw+PDNzJk7GEGRTG6fWZz\nSQ96Cu9tvcGHiBXwjLlnK+PRWU5IsCiLsjT4xBXsMLMw3YOdMK5z58sqr+SnNEyq\nRHoEvi9aC94WrargVB45Yx+81YNW8uQ5rMDmYaJC5a7ENz522SlAuf4T+fAGJ/HE\n/qbZGD4YwlLqAFDgewQ+5tEWEus3zgY2MIR7vN2zXU1Ptk+mQkXZl/Pxdp7q1xU+\nvr/kcykCgYEAy7MiIAzc1ctQDvkk3HiespzdQ/sC7+CGsBzkyubRc9Oq/YR7GfVK\nGTuDEDlWwx92VAvJGDWRa3T426YDyqiPj66uo836sgL15Uigg5afZun2bqGC78le\nBhSy9b+0YDHPa87GxtKt9UmMoB6WdmoPzOkLEEGS7eesmk2DDgY+QSUCgYEA8tr/\n3PawigL1cxuFpcO1lH6XUspGeAo5yB8FXvfW5g50e37LgooIvOFgUlYuchxwr6uh\nW+CUAWmm4farsgvMBMPYw+PbkCTi/xemiiDmMHUYd7sJkTl0JXApq3pZsNMg4Fw/\n29RynmcG8TGe2dkwrWp1aBYjvIHwEHuNHHTTA0sCgYBtSUFAwsXkaj0cm2y8YHZ8\nS46mv1AXFHYOnKHffjDXnLN7ao2FIsXLfdNWa/zxmLqqYtxUAcFwToSJi6szGnZT\nVxvZRFSBFveIOQvtLW1+EH4nYr3WGko4pvhQwrZqea7YH0skNrogBILPEToWc9bg\nUBOgeB31R7uh2X47kvvphQKBgQDWc60dYnniZVp5mwQZrQjbaC4YXaZ8ugrsPPhx\nNEoAPSN/KihrzZiJsjtsec3p1lNrzRNgHqCT3sgPIdPcFa7DRm5UDRIF54zL1gaq\nUwLyJ3TDxdZc928o4DLryc8J5mZRuSRq6t+MIU5wDnFHzhK+EBQ9Jc/I1rU22ONz\nDXaIoQKBgH14Apggo0o4Eo+OnEBRFbbDulaOfVLPTK9rktikbwO1vzDch8kdcwCU\nsvtRXHjDQL93Ih/8S9aDJZoSDulwr3VUsuDiDEb4jfYmP2sbNO4nIJt+SBMhVOXV\nt7E/uWK28X0GL/bIUzSMMgTfdjhXEtJW+s6hQU1fG+9U1qVTQ2R/\n-----END RSA PRIVATE KEY-----\n",
"appId": "181963751145079041",
"clientId": "181963751145144577@zitadel_rust_test"
}"#;

#[rocket::get("/unauthed")]
fn unauthed() -> &'static str {
"Hello Unauthorized User"
}

#[rocket::get("/authed")]
fn authed(user: &IntrospectedUser) -> String {
format!(
"Hello Authorized {:?} with id {}",
user.username, user.user_id
)
}

#[rocket::launch]
async fn rocket() -> _ {
rocket::build()
.mount("/", rocket::routes![unauthed, authed])
.manage(
IntrospectionConfigBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud")
.with_jwt_profile(Application::load_from_json(APPLICATION).unwrap())
.build()
.await
.unwrap(),
)
}
2 changes: 1 addition & 1 deletion external/googleapis
Submodule googleapis updated 489 files
2 changes: 1 addition & 1 deletion external/grpc-gateway
Submodule grpc-gateway updated 78 files
+1 −1 .bazelversion
+1 −1 .github/Dockerfile
+1 −1 .github/plugins/protoc-gen-grpc-gateway/Dockerfile
+1 −1 .github/plugins/protoc-gen-openapiv2/Dockerfile
+16 −16 .github/workflows/ci.yml
+2 −2 .github/workflows/main.yml
+4 −4 .github/workflows/release.yml
+5 −5 .github/workflows/renovate.yml
+2 −2 CONTRIBUTING.md
+7 −7 WORKSPACE
+3 −3 docs/Gemfile.lock
+4 −4 docs/docs/tutorials/introduction.md
+3 −3 examples/internal/browser/package-lock.json
+0 −214 examples/internal/clients/echo/api/swagger.yaml
+0 −337 examples/internal/clients/echo/api_echo_service.go
+0 −1 examples/internal/clients/echo/model_examplepb_simple_message.go
+0 −20 examples/internal/clients/unannotatedecho/api/swagger.yaml
+0 −15 examples/internal/clients/unannotatedecho/api_unannotated_echo_service.go
+0 −1 examples/internal/clients/unannotatedecho/model_examplepb_unannotated_simple_message.go
+10 −10 examples/internal/helloworld/helloworld.pb.gw.go
+0 −1 examples/internal/helloworld/helloworld.swagger.json
+1 −74 examples/internal/integration/integration_test.go
+14 −14 examples/internal/proto/examplepb/a_bit_of_everything.pb.gw.go
+0 −13 examples/internal/proto/examplepb/a_bit_of_everything.swagger.json
+90 −105 examples/internal/proto/examplepb/echo_service.pb.go
+7 −265 examples/internal/proto/examplepb/echo_service.pb.gw.go
+0 −8 examples/internal/proto/examplepb/echo_service.proto
+0 −237 examples/internal/proto/examplepb/echo_service.swagger.json
+14 −14 examples/internal/proto/examplepb/flow_combination.pb.gw.go
+0 −1 examples/internal/proto/examplepb/flow_combination.swagger.json
+0 −1 examples/internal/proto/examplepb/generate_unbound_methods.swagger.json
+0 −3 examples/internal/proto/examplepb/generated_input.swagger.json
+2 −2 examples/internal/proto/examplepb/non_standard_names.pb.gw.go
+0 −1 examples/internal/proto/examplepb/non_standard_names.swagger.json
+0 −1 examples/internal/proto/examplepb/openapi_merge.swagger.json
+0 −1 examples/internal/proto/examplepb/openapi_merge_a.swagger.json
+0 −1 examples/internal/proto/examplepb/openapi_merge_b.swagger.json
+0 −3 examples/internal/proto/examplepb/response_body_service.swagger.json
+0 −4 examples/internal/proto/examplepb/stream.swagger.json
+33 −44 examples/internal/proto/examplepb/unannotated_echo_service.pb.go
+2 −2 examples/internal/proto/examplepb/unannotated_echo_service.pb.gw.go
+0 −1 examples/internal/proto/examplepb/unannotated_echo_service.proto
+0 −22 examples/internal/proto/examplepb/unannotated_echo_service.swagger.json
+0 −1 examples/internal/proto/examplepb/use_go_template.swagger.json
+2 −2 examples/internal/proto/examplepb/visibility_rule_echo_service.pb.gw.go
+0 −1 examples/internal/proto/examplepb/visibility_rule_internal_echo_service.swagger.json
+0 −1 examples/internal/proto/examplepb/visibility_rule_none_echo_service.swagger.json
+0 −1 examples/internal/proto/examplepb/visibility_rule_preview_and_internal_echo_service.swagger.json
+0 −1 examples/internal/proto/examplepb/visibility_rule_preview_echo_service.swagger.json
+0 −1 examples/internal/proto/examplepb/wrappers.swagger.json
+0 −1 examples/internal/proto/oneofenum/oneof_enum.swagger.json
+0 −1 examples/internal/proto/pathenum/path_enum.swagger.json
+5 −5 examples/internal/proto/standalone/unannotated_echo_service.pb.gw.go
+0 −1 examples/internal/proto/sub/message.swagger.json
+0 −1 examples/internal/proto/sub2/message.swagger.json
+5 −5 go.mod
+10 −15 go.sum
+0 −1 internal/descriptor/apiconfig/apiconfig.swagger.json
+0 −1 internal/descriptor/openapiconfig/openapiconfig.swagger.json
+0 −14 internal/descriptor/registry.go
+4 −1 protoc-gen-grpc-gateway/BUILD.bazel
+0 −8 protoc-gen-grpc-gateway/internal/gengateway/template.go
+2 −12 protoc-gen-openapiv2/defs.bzl
+0 −21 protoc-gen-openapiv2/internal/genopenapi/template.go
+0 −296 protoc-gen-openapiv2/internal/genopenapi/template_test.go
+6 −23 protoc-gen-openapiv2/internal/genopenapi/types.go
+0 −2 protoc-gen-openapiv2/main.go
+0 −1 protoc-gen-openapiv2/options/annotations.swagger.json
+0 −1 protoc-gen-openapiv2/options/openapiv2.swagger.json
+14 −27 repositories.bzl
+1 −1 runtime/fieldmask.go
+1 −6 runtime/fieldmask_test.go
+125 −138 runtime/internal/examplepb/example.pb.go
+0 −1 runtime/internal/examplepb/example.proto
+0 −1 runtime/internal/examplepb/example.swagger.json
+0 −1 runtime/internal/examplepb/non_standard_names.swagger.json
+0 −1 runtime/internal/examplepb/proto2.swagger.json
+0 −1 runtime/internal/examplepb/proto3.swagger.json
2 changes: 1 addition & 1 deletion external/protoc-gen-validate
2 changes: 1 addition & 1 deletion external/zitadel
3 changes: 0 additions & 3 deletions src/api/mod.rs
Expand Up @@ -5,12 +5,9 @@
//! Further contains interceptors that may be used to
//! authenticate the clients to ZITADEL with credentials.

#[cfg(feature = "api")]
pub use api::zitadel;

#[cfg(feature = "api")]
mod api;
#[cfg(any(feature = "api", feature = "interceptors", feature = "credentials"))]
pub mod clients;
#[cfg(feature = "interceptors")]
pub mod interceptors;
4 changes: 4 additions & 0 deletions src/lib.rs
Expand Up @@ -39,3 +39,7 @@
pub mod api;
#[cfg(feature = "credentials")]
pub mod credentials;
#[cfg(feature = "oidc")]
pub mod oidc;
#[cfg(feature = "rocket")]
pub mod rocket;
128 changes: 128 additions & 0 deletions src/oidc/discovery.rs
@@ -0,0 +1,128 @@
use custom_error::custom_error;
use openidconnect::reqwest::async_http_client;
use openidconnect::{
core::{
CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType,
CoreJsonWebKey, CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm,
CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, CoreResponseMode, CoreResponseType,
CoreSubjectIdentifierType,
},
url, AdditionalProviderMetadata, IntrospectionUrl, IssuerUrl, ProviderMetadata, RevocationUrl,
};
use serde::{Deserialize, Serialize};

custom_error! {
/// Error type for discovery related errors.
pub DiscoveryError
IssuerUrl{source: url::ParseError} = "could not parse issuer url: {source}",
DiscoveryDocument = "could not discover OIDC document",
}

/// Fetch the well-known [OIDC Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)
/// document of a given `authority`. "Authority" is a synonym for "Issuer" and vice versa.
/// The discovery document contains information about various OIDC endpoints of the ZITADEL
/// instance. Note that the authority (issuer) must not contain the well-known url part
/// (`/.well-known/openid-configuration`).
///
/// The returned [metadata](ZitadelProviderMetadata) contains the parsed information of the
/// well-known OIDC configuration.
///
/// ### Errors
///
/// This method may fail if:
/// - The authority url cannot be parsed correctly
/// - The discovery call throws any kind of error
///
/// ### Example
///
/// #### Fetch the discovery document of the "ZITADEL Libraries" - Test Instance
///
/// ```
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>>{
/// use zitadel::oidc::discovery::discover;
/// let authority = "https://zitadel-libraries-l8boqa.zitadel.cloud";
/// let metadata = discover(authority).await?;
/// println!("{:?}", metadata.token_endpoint());
/// # Ok(())
/// # }
/// ```
pub async fn discover(authority: &str) -> Result<ZitadelProviderMetadata, DiscoveryError> {
let issuer = IssuerUrl::new(authority.to_string())
.map_err(|source| DiscoveryError::IssuerUrl { source })?;
ZitadelProviderMetadata::discover_async(issuer, async_http_client)
.await
.map_err(|_| DiscoveryError::DiscoveryDocument)
}

/// Definition of additional metadata that is not present in the
/// standard metadata of the openidconnect crate.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ZitadelAdditionalMetadata {
pub introspection_endpoint: Option<IntrospectionUrl>,
pub revocation_endpoint: Option<RevocationUrl>,
}

impl AdditionalProviderMetadata for ZitadelAdditionalMetadata {}

/// Type to the ZITADEL provider metadata. Essentially combines
/// the [ZitadelAdditionalMetadata] with the openid provider
/// metadata information.
pub type ZitadelProviderMetadata = ProviderMetadata<
ZitadelAdditionalMetadata,
CoreAuthDisplay,
CoreClientAuthMethod,
CoreClaimName,
CoreClaimType,
CoreGrantType,
CoreJweContentEncryptionAlgorithm,
CoreJweKeyManagementAlgorithm,
CoreJwsSigningAlgorithm,
CoreJsonWebKeyType,
CoreJsonWebKeyUse,
CoreJsonWebKey,
CoreResponseMode,
CoreResponseType,
CoreSubjectIdentifierType,
>;

#[cfg(test)]
mod tests {
#![allow(clippy::all)]

use super::*;

const ZITADEL_URL: &str = "https://zitadel-libraries-l8boqa.zitadel.cloud";

#[tokio::test]
async fn discovery_fails_with_invalid_url() {
let result = discover("foobar").await;

assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DiscoveryError::IssuerUrl { .. }
));
}

#[tokio::test]
async fn discovery_fails_with_invalid_discovery() {
let result = discover("https://smartive.ch").await;

assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DiscoveryError::DiscoveryDocument
));
}

#[tokio::test]
async fn discovery_succeeds() {
let result = discover(ZITADEL_URL).await.unwrap();

assert_eq!(
result.token_endpoint().unwrap().to_string(),
"https://zitadel-libraries-l8boqa.zitadel.cloud/oauth/v2/token".to_string()
);
}
}

0 comments on commit ea04fd6

Please sign in to comment.