#[cfg(feature = "oidc")]
mod run {
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64_URL;
use base64::Engine;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use sha2::{Digest, Sha256};
use solid_pod_rs::oidc::{
discovery_for, register_client, verify_access_token, verify_dpop_proof,
ClientRegistrationRequest, CnfClaim, Jwk, SolidOidcClaims,
};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let issuer = "https://op.example";
let discovery = discovery_for(issuer);
println!(
"[discovery] issuer={} token={}",
discovery.issuer, discovery.token_endpoint
);
let reg_req = ClientRegistrationRequest {
redirect_uris: vec!["https://app.example/cb".into()],
client_name: Some("solid-pod-rs example".into()),
client_uri: Some("https://app.example".into()),
grant_types: vec!["authorization_code".into()],
response_types: vec!["code".into()],
scope: Some("openid webid offline_access".into()),
token_endpoint_auth_method: Some("none".into()),
application_type: Some("web".into()),
};
let now = 1_700_000_000u64;
let reg_resp = register_client(®_req, now);
println!("[register] client_id={}", reg_resp.client_id);
let dpop_secret = b"dpop-demo-secret";
let jwk = Jwk {
kty: "oct".into(),
alg: Some("HS256".into()),
kid: None,
use_: None,
crv: None,
x: None,
y: None,
n: None,
e: None,
k: Some(BASE64_URL.encode(dpop_secret)),
};
let jkt = jwk.thumbprint()?;
println!("[dpop] jkt={jkt}");
let dpop_proof = build_dpop_proof(
dpop_secret,
&jwk,
"https://pod.example/resource",
"GET",
now,
)?;
#[cfg(feature = "dpop-replay-cache")]
let verified_dpop = {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(verify_dpop_proof(
&dpop_proof,
"https://pod.example/resource",
"GET",
now,
60,
None,
))?
};
#[cfg(not(feature = "dpop-replay-cache"))]
let verified_dpop = verify_dpop_proof(
&dpop_proof,
"https://pod.example/resource",
"GET",
now,
60,
)?;
assert_eq!(verified_dpop.jkt, jkt);
let at_secret = b"at-demo-secret";
let at = issue_access_token(at_secret, issuer, &jkt, now + 3600, now)?;
let keyset = solid_pod_rs::oidc::TokenVerifyKey::Symmetric(at_secret.to_vec());
let verified = verify_access_token(
&at,
&keyset,
issuer,
&verified_dpop.jkt,
now,
)?;
println!(
"[verify] webid={} client={}",
verified.webid,
verified.client_id.as_deref().unwrap_or("<none>"),
);
Ok(())
}
fn build_dpop_proof(
secret: &[u8],
jwk: &Jwk,
htu: &str,
htm: &str,
iat: u64,
) -> Result<String, Box<dyn std::error::Error>> {
let header_json = serde_json::json!({
"typ": "dpop+jwt",
"alg": "HS256",
"jwk": jwk,
});
let claims = serde_json::json!({
"htu": htu,
"htm": htm,
"iat": iat,
"jti": uuid::Uuid::new_v4().to_string(),
});
let h_b64 = BASE64_URL.encode(serde_json::to_vec(&header_json)?);
let p_b64 = BASE64_URL.encode(serde_json::to_vec(&claims)?);
let signing_input = format!("{h_b64}.{p_b64}");
let sig = hmac_sha256_b64url(secret, signing_input.as_bytes());
Ok(format!("{signing_input}.{sig}"))
}
fn hmac_sha256_b64url(secret: &[u8], msg: &[u8]) -> String {
const BLOCK: usize = 64;
let mut key = [0u8; BLOCK];
if secret.len() > BLOCK {
let h = Sha256::digest(secret);
key[..h.len()].copy_from_slice(&h);
} else {
key[..secret.len()].copy_from_slice(secret);
}
let mut ipad = [0u8; BLOCK];
let mut opad = [0u8; BLOCK];
for i in 0..BLOCK {
ipad[i] = key[i] ^ 0x36;
opad[i] = key[i] ^ 0x5c;
}
let mut inner = Sha256::new();
inner.update(ipad);
inner.update(msg);
let inner_digest = inner.finalize();
let mut outer = Sha256::new();
outer.update(opad);
outer.update(inner_digest);
BASE64_URL.encode(outer.finalize())
}
fn issue_access_token(
secret: &[u8],
issuer: &str,
jkt: &str,
exp: u64,
iat: u64,
) -> Result<String, Box<dyn std::error::Error>> {
let claims = SolidOidcClaims {
iss: issuer.to_string(),
sub: "https://me.example/profile#me".into(),
aud: serde_json::json!("solid"),
exp,
iat,
webid: Some("https://me.example/profile#me".into()),
client_id: Some("client-123".into()),
cnf: Some(CnfClaim { jkt: jkt.to_string() }),
scope: Some("openid webid".into()),
};
Ok(encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret(secret),
)?)
}
}
#[cfg(feature = "oidc")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
run::main()
}
#[cfg(not(feature = "oidc"))]
fn main() {
eprintln!("oidc_client requires --features oidc (re-run with `--features oidc`)");
}