authly_client/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! `authly-client` is an asynchronous Rust client handle for services interfacing with the authly service.

#![forbid(unsafe_code)]
#![warn(missing_docs)]

use access_control::AccessControlRequestBuilder;
use arc_swap::ArcSwap;

use std::{borrow::Cow, sync::Arc};

use anyhow::anyhow;
use authly_common::{
    access_token::AuthlyAccessTokenClaims,
    id::Eid,
    proto::service::{self as proto, authly_service_client::AuthlyServiceClient},
    service::PropertyMapping,
};
use http::header::COOKIE;
use token::AccessToken;
use tonic::Request;

pub use builder::ClientBuilder;
pub use error::Error;

/// Client identity.
pub mod identity;

/// Token utilities.
pub mod token;

pub mod access_control;

mod builder;
mod error;

/// File path for detecting a valid kubernetes environment.
const K8S_SA_TOKENFILE: &str = "/var/run/secrets/kubernetes.io/serviceaccount/token";

/// The authly client handle.
#[derive(Clone)]
pub struct Client {
    inner: Arc<ClientInner>,
}

/// Shared data for cloned clients
struct ClientInner {
    service: AuthlyServiceClient<tonic::transport::Channel>,
    jwt_decoding_key: jsonwebtoken::DecodingKey,

    /// The resource property mapping for this service.
    /// It's kept in an ArcSwap to potentially support live-update of this structure.
    /// For that to work, the client should keep a subscription option and listen
    /// for change events and re-download the property mapping.
    resource_property_mapping: Arc<ArcSwap<PropertyMapping>>,
}

impl Client {
    /// Construct a new builder.
    pub fn builder() -> ClientBuilder {
        ClientBuilder {
            authly_local_ca: None,
            identity: None,
            jwt_decoding_key: None,
            url: Cow::Borrowed("https://authly"),
        }
    }

    /// The eid of this client.
    pub async fn entity_id(&self) -> Result<Eid, Error> {
        let mut service = self.inner.service.clone();
        let metadata = service
            .get_metadata(proto::Empty::default())
            .await
            .map_err(error::tonic)?
            .into_inner();

        Eid::from_bytes(&metadata.entity_id).ok_or_else(id_codec_error)
    }

    /// The name of this client.
    pub async fn label(&self) -> Result<String, Error> {
        let mut service = self.inner.service.clone();
        let metadata = service
            .get_metadata(proto::Empty::default())
            .await
            .map_err(error::tonic)?
            .into_inner();

        Ok(metadata.label)
    }

    /// Make a new access control request, returning a builder for building it.
    pub fn access_control_request(&self) -> AccessControlRequestBuilder<'_> {
        AccessControlRequestBuilder::new(self)
    }

    /// Exchange a session token for an access token suitable for evaluating access control.
    pub async fn get_access_token(&self, session_token: &str) -> Result<Arc<AccessToken>, Error> {
        let mut service = self.inner.service.clone();
        let mut request = Request::new(proto::Empty::default());

        // TODO: This should use Authorization instead of Cookie?
        request.metadata_mut().append(
            COOKIE.as_str(),
            format!("session-cookie={session_token}")
                .parse()
                .map_err(error::unclassified)?,
        );

        let proto = service
            .get_access_token(request)
            .await
            .map_err(error::tonic)?
            .into_inner();

        let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::ES256);
        let token_data = jsonwebtoken::decode::<AuthlyAccessTokenClaims>(
            &proto.token,
            &self.inner.jwt_decoding_key,
            &validation,
        )
        .map_err(|err| Error::InvalidAccessToken(err.into()))?;

        Ok(Arc::new(AccessToken {
            token: proto.token,
            claims: token_data.claims,
        }))
    }
}

fn id_codec_error() -> Error {
    Error::Codec(anyhow!("id decocing error"))
}