Skip to content

Commit 081f0e4

Browse files
committed
feat: test integration with OIDC provider and few protocol changes
1 parent bd749a5 commit 081f0e4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3838
-1542
lines changed

acme/src/account.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::jws::AcmeJws;
21
use crate::prelude::*;
32
use rusty_jwt_tools::prelude::*;
43

acme/src/authz.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
use crate::{
2-
account::AcmeAccount,
3-
chall::{AcmeChallenge, AcmeChallengeType},
4-
jws::AcmeJws,
5-
prelude::*,
6-
};
1+
use crate::chall::AcmeChallengeType;
2+
use crate::prelude::*;
73
use rusty_jwt_tools::prelude::*;
84

95
impl RustyAcme {
@@ -37,7 +33,7 @@ impl RustyAcme {
3733
AuthzStatus::Expired => return Err(AcmeAuthzError::Expired)?,
3834
AuthzStatus::Valid => {
3935
return Err(RustyAcmeError::ClientImplementationError(
40-
"An authorization is not supposed to be valid at this point. \
36+
"an authorization is not supposed to be valid at this point. \
4137
You should only use this method to parse the response of an authorization creation.",
4238
))
4339
}
@@ -64,8 +60,7 @@ pub enum AcmeAuthzError {
6460

6561
/// Result of an authorization creation
6662
/// see [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5)
67-
#[derive(Debug, serde::Serialize, serde::Deserialize)]
68-
#[cfg_attr(test, derive(Clone))]
63+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
6964
#[serde(rename_all = "camelCase")]
7065
pub struct AcmeAuthz {
7166
/// Should be pending for a newly created authorization

acme/src/certificate.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{account::AcmeAccount, finalize::AcmeFinalize, jws::AcmeJws, prelude::*};
1+
use crate::prelude::*;
22
use rusty_jwt_tools::prelude::*;
33

44
impl RustyAcme {
@@ -29,7 +29,6 @@ impl RustyAcme {
2929
.split(Self::CERTIFICATE_BEGIN)
3030
.filter(|c| !c.is_empty())
3131
.map(|c| c.trim().trim_end_matches(Self::CERTIFICATE_END).trim())
32-
.into_iter()
3332
.try_fold(vec![], |mut acc, cert_pem| -> RustyAcmeResult<Vec<String>> {
3433
Self::parse_x509_and_validate(cert_pem)?;
3534
acc.push(cert_pem.to_string());

acme/src/chall.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
use crate::account::AcmeAccount;
2-
use crate::jws::AcmeJws;
31
use crate::prelude::*;
2+
use jwt_simple::prelude::*;
43
use rusty_jwt_tools::prelude::*;
54

65
impl RustyAcme {
@@ -27,19 +26,27 @@ impl RustyAcme {
2726

2827
/// oidc challenge request to `POST /acme/challenge/{token}`
2928
/// see [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1)
29+
#[allow(clippy::too_many_arguments)]
3030
pub fn oidc_chall_request(
3131
id_token: String,
3232
oidc_chall: AcmeChallenge,
3333
account: &AcmeAccount,
3434
alg: JwsAlgorithm,
35+
hash_alg: HashAlgorithm,
3536
kp: &Pem,
37+
jwk: &Jwk,
3638
previous_nonce: String,
3739
) -> RustyAcmeResult<AcmeJws> {
3840
// Extract the account URL from previous response which created a new account
3941
let acct_url = account.acct_url()?;
4042

43+
let thumbprint = JwkThumbprint::generate(jwk, hash_alg)?.kid;
44+
let chall_token = oidc_chall.token;
45+
let keyauth = format!("{chall_token}.{thumbprint}");
46+
4147
let payload = Some(serde_json::json!({
4248
"id_token": id_token,
49+
"keyauth": keyauth,
4350
}));
4451
let req = AcmeJws::new(alg, previous_nonce, &oidc_chall.url, Some(&acct_url), payload, kp)?;
4552
Ok(req)
@@ -55,13 +62,13 @@ impl RustyAcme {
5562
Some(AcmeChallengeStatus::Invalid) => return Err(AcmeChallError::Invalid)?,
5663
Some(AcmeChallengeStatus::Pending) => {
5764
return Err(RustyAcmeError::ClientImplementationError(
58-
"A challenge is not supposed to be pending at this point. \
65+
"a challenge is not supposed to be pending at this point. \
5966
It must either be 'valid' or 'processing'.",
6067
))
6168
}
6269
None => {
6370
return Err(RustyAcmeError::ClientImplementationError(
64-
"At this point a challenge is supposed to have a status",
71+
"at this point a challenge is supposed to have a status",
6572
))
6673
}
6774
}

acme/src/directory.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ impl RustyAcme {
1111
}
1212
}
1313

14-
#[derive(Debug, serde::Serialize, serde::Deserialize)]
15-
#[cfg_attr(test, derive(Clone))]
14+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1615
#[serde(rename_all = "camelCase")]
1716
/// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1)
1817
pub struct AcmeDirectory {

acme/src/docker/dex.rs

Lines changed: 111 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,57 @@
1-
use std::collections::HashMap;
1+
use std::{collections::HashMap, net::SocketAddr, path::PathBuf};
2+
23
use testcontainers::{clients::Cli, core::WaitFor, Container, Image, RunnableImage};
34

5+
use crate::{docker::ldap::LdapCfg, docker::rand_str};
6+
7+
pub struct DexServer<'a> {
8+
pub uri: String,
9+
pub node: Container<'a, DexImage>,
10+
pub socket: SocketAddr,
11+
}
12+
413
#[derive(Debug)]
514
pub struct DexImage {
615
pub volumes: HashMap<String, String>,
716
pub env_vars: HashMap<String, String>,
17+
pub cfg_file: PathBuf,
818
}
919

1020
impl DexImage {
1121
const NAME: &'static str = "dexidp/dex";
12-
const TAG: &'static str = "latest";
22+
const TAG: &'static str = "v2.35.3";
1323
pub const PORT: u16 = 5556;
1424

15-
pub fn run<'a>(docker: &'a Cli, host: &str) -> (u16, Container<'a, DexImage>) {
16-
let instance = Self::default();
25+
pub fn run(docker: &Cli, cfg: DexCfg, redirect_uri: String) -> DexServer {
26+
let instance = Self::new(&cfg, &redirect_uri);
1727
let image: RunnableImage<Self> = instance.into();
18-
let image = image.with_container_name(host).with_network(super::NETWORK);
28+
let image = image
29+
.with_container_name(&cfg.host)
30+
.with_network(super::NETWORK)
31+
.with_mapped_port((cfg.host_port, Self::PORT));
1932
let node = docker.run(image);
2033
let port = node.get_host_port_ipv4(Self::PORT);
21-
(port, node)
34+
let uri = format!("http://{}:{port}", cfg.host);
35+
36+
let ip = std::net::IpAddr::V4("127.0.0.1".parse().unwrap());
37+
let socket = SocketAddr::new(ip, port);
38+
39+
DexServer { uri, socket, node }
40+
}
41+
42+
pub fn new(cfg: &DexCfg, redirect_uri: &str) -> Self {
43+
let host_vol = std::env::temp_dir().join(rand_str());
44+
std::fs::create_dir(&host_vol).unwrap();
45+
let host_cfg_file = host_vol.join("config.docker.yaml");
46+
47+
std::fs::write(&host_cfg_file, cfg.to_yaml(redirect_uri)).unwrap();
48+
49+
let host_vol_str = host_cfg_file.as_os_str().to_str().unwrap().to_string();
50+
Self {
51+
volumes: HashMap::from_iter(vec![(host_vol_str, "/etc/dex/config.docker.yaml".to_string())]),
52+
env_vars: HashMap::new(),
53+
cfg_file: host_cfg_file,
54+
}
2255
}
2356
}
2457

@@ -34,7 +67,8 @@ impl Image for DexImage {
3467
}
3568

3669
fn ready_conditions(&self) -> Vec<WaitFor> {
37-
vec![WaitFor::message_on_stderr("listening (http) on 0.0.0.0:5556")]
70+
let msg = format!("listening (http) on 0.0.0.0:{}", Self::PORT);
71+
vec![WaitFor::message_on_stderr(msg)]
3872
}
3973

4074
fn volumes(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> {
@@ -46,23 +80,78 @@ impl Image for DexImage {
4680
}
4781
}
4882

49-
impl Default for DexImage {
50-
fn default() -> Self {
51-
Self {
52-
volumes: HashMap::from_iter(vec![]),
53-
env_vars: HashMap::default(),
54-
}
55-
}
83+
#[derive(Debug, Clone)]
84+
pub struct DexCfg {
85+
pub client_id: String,
86+
pub client_secret: String,
87+
pub issuer: String,
88+
pub ldap_host: String,
89+
pub domain: String,
90+
pub host_port: u16,
91+
pub host: String,
5692
}
5793

58-
#[test]
59-
fn test_dex() {
60-
let docker = Cli::docker();
61-
let (port, _dex) = DexImage::run(&docker, "dex");
94+
impl DexCfg {
95+
pub fn to_yaml(&self, redirect_uri: &str) -> String {
96+
let Self {
97+
client_id,
98+
client_secret,
99+
issuer,
100+
ldap_host,
101+
domain,
102+
..
103+
} = self;
104+
let domain = LdapCfg::domain_to_ldif(domain);
105+
format!(
106+
r#"
107+
issuer: {issuer}
108+
storage:
109+
type: memory
110+
web:
111+
http: 0.0.0.0:5556
112+
logger:
113+
level: "debug"
114+
format: "text"
62115
63-
let uri = format!("http://localhost:{port}/dex/.well-known/openid-configuration");
116+
oauth2:
117+
skipApprovalScreen: false
118+
alwaysShowLoginScreen: false
64119
65-
let response = reqwest::blocking::get(uri).unwrap();
66-
let body = response.json::<serde_json::Value>().unwrap();
67-
println!("{}", serde_json::to_string_pretty(&body).unwrap());
120+
# expiry:
121+
# deviceRequests: "5m"
122+
# signingKeys: "6h"
123+
# idTokens: "24h"
124+
# refreshTokens:
125+
# reuseInterval: "3s"
126+
# validIfNotUsedFor: "2160h" # 90 days
127+
# absoluteLifetime: "3960h" # 165 days
128+
129+
connectors:
130+
- type: ldap
131+
name: OpenLDAP
132+
id: ldap
133+
config:
134+
host: {ldap_host}:389
135+
insecureNoSSL: true
136+
insecureSkipVerify: true
137+
bindDN: cn=admin,{domain}
138+
bindPW: admin
139+
usernamePrompt: Email Address
140+
userSearch:
141+
baseDN: ou=People,{domain}
142+
filter: "(objectClass=person)"
143+
username: mail
144+
idAttr: uid
145+
emailAttr: mail
146+
nameAttr: cn
147+
preferredUsernameAttr: sn
148+
staticClients:
149+
- id: {client_id}
150+
redirectURIs:
151+
- '{redirect_uri}'
152+
name: 'Example App'
153+
secret: {client_secret}
154+
"#
155+
)
156+
}
68157
}

0 commit comments

Comments
 (0)