diff --git a/src/main.rs b/src/main.rs index 0b267aa..5b1bbc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -404,23 +404,27 @@ async fn authenticate_endpoint(mut req: Request) -> tide::Result { } } - // The token is random because there are no resources protected by the token anyway. - let mut access_token = [0u8; 32]; - SystemRandom::new().fill(&mut access_token)?; - let access_token = base64_coder::URL_SAFE_NO_PAD.encode(&access_token); + let id_token = create_id_token( + req.state(), + &credentials.0, + &code_info.account, + code_info.nonce, + )?; req.state() .successful_logins .fetch_add(1, Ordering::Relaxed); // give access code - Ok(Response::builder(200).body(json!({ - "access_token": access_token, - "token_type": "Bearer", - "expires_in": TOKEN_EXPIRATION, - "id_token": create_id_token(req.state(), &credentials.0, &code_info.account, code_info.nonce)?, - "scope": "openid profile email" - })).into()) + Ok(Response::builder(200) + .body(json!({ + "access_token": id_token, + "token_type": "Bearer", + "expires_in": TOKEN_EXPIRATION, + "id_token": id_token, + "scope": "openid profile email" + })) + .into()) } async fn jwks_endpoint(req: Request) -> tide::Result { @@ -445,6 +449,7 @@ async fn configuration_endpoint(req: Request) -> tide::Result { "issuer": uri, "authorization_endpoint": uri.join("/authorize")?, "token_endpoint": uri.join("/token")?, + "userinfo_endpoint": uri.join("/userinfo")?, "jwks_uri": uri.join("/jwks")?, "response_types_supported": ["code"], "subject_types_supported": ["pairwise"], @@ -493,6 +498,60 @@ logins_success {} .into()) } +/// We expect the ID token as our access token +async fn userinfo_endpoint(req: Request) -> tide::Result { + // this is all wrapped, because if something fails, then its the + // client's fault anyways (if there aren't any bugs, oh well...) + let resp = || { + let jwt = req + .header("Authorization") + .and_then(|v| v.get(0).unwrap().as_str().strip_prefix("Bearer ")) + .ok_or(anyhow!("no bearer token"))?; // extract token + + let (message, signature) = jwt.rsplit_once(".").ok_or(anyhow!("bad token form"))?; + + let pk: ring::rsa::PublicKeyComponents> = req.state().signing_key.public().into(); + + pk.verify( + &ring::signature::RSA_PKCS1_2048_8192_SHA256, + message.as_bytes(), + &base64_coder::URL_SAFE_NO_PAD.decode(signature)?, + )?; + + let (_header, claims) = message.rsplit_once(".").ok_or(anyhow!("bad token form"))?; + let decoded_claims = base64_coder::URL_SAFE_NO_PAD.decode(claims)?; + let decoded_claims: serde_json::Value = serde_json::from_slice(&decoded_claims)?; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("no time travelling allowed before 1970") + .as_secs(); + + if decoded_claims["exp"] + .as_u64() + .ok_or(anyhow!("invalid exp"))? + < now + { + Err(anyhow!("token expired")) + } else { + Ok(Response::builder(200) + .content_type("application/json") + .body(decoded_claims) + .build()) + } + }; + + #[allow(unused_variables)] // rustc complains when building in release mode + resp().or_else(|e| { + #[cfg(debug_assertions)] + log::debug!("userinfo error: {}", e); + + Ok(Response::builder(401) + .header("WWW-Authenticate", "Bearer error=\"invalid_token\"") + .build()) + }) +} + pub struct AuthStore { pub auths: HashMap, pub expirations: LinkedList<(String, u64)>, @@ -563,6 +622,7 @@ async fn main() -> Result<()> { .get(configuration_endpoint); app.at("/new-account").get(create_account_endpoint); app.at("/metrics").get(metrics_endpoint); + app.at("/userinfo").get(userinfo_endpoint); auto_serve_dir!(app, "/static", "static");