|
@@ -1,10 +1,10 @@
|
|
|
-use crate::{config, key, schema, UIDCError};
|
|
|
+use crate::{key, schema, server::session::SessionHelper};
|
|
|
use microrm::prelude::*;
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
-use super::session::SessionHelper;
|
|
|
-
|
|
|
mod api;
|
|
|
+mod authorize;
|
|
|
+mod token;
|
|
|
|
|
|
type Request = tide::Request<super::ServerStateWrapper>;
|
|
|
|
|
@@ -58,176 +58,6 @@ impl<'a> From<microrm::Error> for OIDCError<'a> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-#[derive(Deserialize)]
|
|
|
-struct AuthorizeQueryParams {
|
|
|
- response_type: String,
|
|
|
- client_id: String,
|
|
|
- redirect_uri: String,
|
|
|
- scope: Option<String>,
|
|
|
-}
|
|
|
-
|
|
|
-fn do_code_authorize<'l, 's>(
|
|
|
- config: &config::Config,
|
|
|
- realm: µrm::Stored<schema::Realm>,
|
|
|
- client: µrm::Stored<schema::Client>,
|
|
|
- user: µrm::Stored<schema::User>,
|
|
|
- scopes: impl Iterator<Item = &'l str>,
|
|
|
- redirect_uri: String,
|
|
|
- state: Option<&'s str>,
|
|
|
-) -> Result<tide::Response, OIDCError<'s>> {
|
|
|
- let expiry =
|
|
|
- std::time::SystemTime::now() + std::time::Duration::from_secs(config.auth_code_expiry);
|
|
|
-
|
|
|
- let rng = ring::rand::SystemRandom::new();
|
|
|
- let raw_auth_code: [u8; 32] = ring::rand::generate(&rng)
|
|
|
- .map_err(|_| {
|
|
|
- OIDCError(
|
|
|
- OIDCErrorType::ServerError,
|
|
|
- format!("Failed to generate auth code."),
|
|
|
- state,
|
|
|
- )
|
|
|
- })?
|
|
|
- .expose();
|
|
|
- let encoded_auth_code = base64::encode_config(raw_auth_code, base64::URL_SAFE_NO_PAD);
|
|
|
-
|
|
|
- realm.auth_codes.insert(schema::AuthCode {
|
|
|
- realm: realm.id(),
|
|
|
- client: client.id(),
|
|
|
- user: user.id(),
|
|
|
- scopes: scopes
|
|
|
- .map(|x| x.to_owned())
|
|
|
- .collect::<Vec<_>>()
|
|
|
- .into_serialized(),
|
|
|
- expiry: expiry.into(),
|
|
|
- redirect_uri: redirect_uri.clone(),
|
|
|
- code: encoded_auth_code.clone(),
|
|
|
- })?;
|
|
|
-
|
|
|
- let new_params = [
|
|
|
- ("response_type", "code"),
|
|
|
- ("code", encoded_auth_code.as_str()),
|
|
|
- ]
|
|
|
- .into_iter()
|
|
|
- .chain(state.map(|v| ("state", v)));
|
|
|
-
|
|
|
- Ok(tide::Redirect::temporary(
|
|
|
- tide::http::Url::parse_with_params(redirect_uri.as_str(), new_params).map_err(|e| {
|
|
|
- OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- format!("could not parse redirect_uri as a URL: {e}"),
|
|
|
- None,
|
|
|
- )
|
|
|
- })?,
|
|
|
- )
|
|
|
- .into())
|
|
|
-}
|
|
|
-
|
|
|
-fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response, OIDCError> {
|
|
|
- let shelper = SessionHelper::new(&request);
|
|
|
- let realm = shelper.get_realm().map_err(|_| {
|
|
|
- OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- "No such realm!".to_string(),
|
|
|
- state,
|
|
|
- )
|
|
|
- })?;
|
|
|
-
|
|
|
- let make_redirect = || {
|
|
|
- let mut login_url = request.url().join("../v1/session/login").unwrap();
|
|
|
- login_url
|
|
|
- .query_pairs_mut()
|
|
|
- .clear()
|
|
|
- .append_pair("redirect", request.url().as_str());
|
|
|
- return Ok(tide::Redirect::new(login_url).into());
|
|
|
- };
|
|
|
-
|
|
|
- let qp: AuthorizeQueryParams = request
|
|
|
- .query()
|
|
|
- .map_err(|x| OIDCError(OIDCErrorType::InvalidRequest, x.to_string(), state))?;
|
|
|
-
|
|
|
- // collect session authentication info
|
|
|
-
|
|
|
- let potential_sauth = shelper
|
|
|
- .get_session(&request)
|
|
|
- .and_then(|session| shelper.get_auth_for_session(realm.id(), &session));
|
|
|
-
|
|
|
- let Some(user_id) = potential_sauth.and_then(|v| v.user) else {
|
|
|
- // if we don't have any relevant auth info, redirect to login
|
|
|
- return make_redirect();
|
|
|
- };
|
|
|
-
|
|
|
- let Ok(Some(user)) = realm
|
|
|
- .users
|
|
|
- .with(schema::UserID::default(), user_id)
|
|
|
- .first()
|
|
|
- .get()
|
|
|
- else {
|
|
|
- return Err(OIDCError(
|
|
|
- OIDCErrorType::ServerError,
|
|
|
- "Internal state error!".to_string(),
|
|
|
- state,
|
|
|
- ));
|
|
|
- };
|
|
|
-
|
|
|
- // verify the realm and client_id and redirect_uri
|
|
|
-
|
|
|
- let client = realm
|
|
|
- .clients
|
|
|
- .with(schema::Client::Shortname, &qp.client_id)
|
|
|
- .first()
|
|
|
- .get()
|
|
|
- .ok()
|
|
|
- .flatten()
|
|
|
- .ok_or_else(|| {
|
|
|
- OIDCError(
|
|
|
- OIDCErrorType::UnauthorizedClient,
|
|
|
- "Client does not exist".to_string(),
|
|
|
- state,
|
|
|
- )
|
|
|
- })?;
|
|
|
-
|
|
|
- let scopes = qp
|
|
|
- .scope
|
|
|
- .as_ref()
|
|
|
- .map(|slist| slist.as_str())
|
|
|
- .unwrap_or("")
|
|
|
- .split_whitespace();
|
|
|
-
|
|
|
- // TODO: check that redirect URI matches
|
|
|
-
|
|
|
- if qp.response_type == "code" {
|
|
|
- do_code_authorize(
|
|
|
- &request.state().core.config,
|
|
|
- &realm,
|
|
|
- &client,
|
|
|
- &user,
|
|
|
- scopes,
|
|
|
- qp.redirect_uri,
|
|
|
- state,
|
|
|
- )
|
|
|
- } else if qp.response_type == "token" {
|
|
|
- let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
|
|
|
-
|
|
|
- let token = rhelper.generate_access_token(&client, &user, scopes).map_err(|e| OIDCError(OIDCErrorType::ServerError, format!("could not generate token: {e}"), state))?;
|
|
|
-
|
|
|
- Ok(tide::Response::builder(200)
|
|
|
- .content_type(tide::http::mime::JSON)
|
|
|
- .body(serde_json::to_vec(&api::TokenResponse {
|
|
|
- token_type: "bearer",
|
|
|
- access_token: token.as_str(),
|
|
|
- refresh_token: None,
|
|
|
- scope: None,
|
|
|
- }).unwrap())
|
|
|
- .build())
|
|
|
- } else {
|
|
|
- Err(OIDCError(
|
|
|
- OIDCErrorType::UnsupportedResponseType,
|
|
|
- "Only code and token are understood.".to_string(),
|
|
|
- state,
|
|
|
- ))
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
async fn authorize(request: Request) -> tide::Result<tide::Response> {
|
|
|
#[derive(Deserialize)]
|
|
|
struct State {
|
|
@@ -235,141 +65,14 @@ async fn authorize(request: Request) -> tide::Result<tide::Response> {
|
|
|
}
|
|
|
let state: Option<String> = request.query::<State>().ok().map(|x| x.state).flatten();
|
|
|
|
|
|
- match do_authorize(request, state.as_ref().map(|x| x.as_str())) {
|
|
|
+ match authorize::do_authorize(request, state.as_ref().map(|x| x.as_str())) {
|
|
|
Ok(r) => Ok(r),
|
|
|
Err(e) => Ok(e.to_response()),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async fn do_token<'l>(mut request: Request) -> Result<tide::Response, OIDCError<'l>> {
|
|
|
- let shelper = SessionHelper::new(&request);
|
|
|
- let realm = shelper
|
|
|
- .get_realm()
|
|
|
- .map_err(|_| OIDCError(OIDCErrorType::InvalidRequest, "no such realm".into(), None))?;
|
|
|
-
|
|
|
- let treq: api::TokenRequestBody = request.body_form().await.map_err(|e| {
|
|
|
- OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- format!("could not parse form body: {e}"),
|
|
|
- None,
|
|
|
- )
|
|
|
- })?;
|
|
|
-
|
|
|
- // TODO: support HTTP basic auth for client authentication instead of treq.client_id
|
|
|
- let client_name = treq.client_id.ok_or(OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- "no client given".into(),
|
|
|
- None,
|
|
|
- ))?;
|
|
|
- let client = realm
|
|
|
- .clients
|
|
|
- .keyed((realm.id(), &client_name))
|
|
|
- .get()?
|
|
|
- .ok_or(OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- format!("unknown client name {client_name}"),
|
|
|
- None,
|
|
|
- ))?;
|
|
|
-
|
|
|
- let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
|
|
|
-
|
|
|
- if treq.grant_type == "authorization_code" {
|
|
|
- let Some(code) = treq.code else { todo!() };
|
|
|
- let code = realm
|
|
|
- .auth_codes
|
|
|
- .keyed((realm.id(), client.id(), code))
|
|
|
- .get()?
|
|
|
- .ok_or(OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- "invalid authorization code".into(),
|
|
|
- None,
|
|
|
- ))?;
|
|
|
-
|
|
|
- let now = std::time::SystemTime::now();
|
|
|
- if code.expiry < now {
|
|
|
- return Err(OIDCError(
|
|
|
- OIDCErrorType::AccessDenied,
|
|
|
- "expired authorization code".into(),
|
|
|
- None,
|
|
|
- ));
|
|
|
- }
|
|
|
-
|
|
|
- let user = realm
|
|
|
- .users
|
|
|
- .with(schema::UserID::default(), code.user)
|
|
|
- .first()
|
|
|
- .get()?
|
|
|
- .ok_or(OIDCError(
|
|
|
- OIDCErrorType::ServerError,
|
|
|
- "could not find user".into(),
|
|
|
- None,
|
|
|
- ))?;
|
|
|
-
|
|
|
- let access_token = rhelper.generate_access_token(
|
|
|
- &client,
|
|
|
- &user,
|
|
|
- code.scopes.as_ref().iter().map(String::as_str),
|
|
|
- )
|
|
|
- .map_err(|e| {
|
|
|
- OIDCError(
|
|
|
- OIDCErrorType::ServerError,
|
|
|
- format!("error signing key: {e}"),
|
|
|
- None,
|
|
|
- )
|
|
|
- })?;
|
|
|
-
|
|
|
- Ok(tide::Response::builder(200)
|
|
|
- .content_type(tide::http::mime::JSON)
|
|
|
- .body(
|
|
|
- serde_json::to_value(api::TokenResponse {
|
|
|
- access_token: access_token.as_str(),
|
|
|
- token_type: "bearer",
|
|
|
- refresh_token: None,
|
|
|
- scope: None,
|
|
|
- })
|
|
|
- .unwrap(),
|
|
|
- )
|
|
|
- .build())
|
|
|
- } else if treq.grant_type == "refresh_token" {
|
|
|
- let Some(rtoken) = treq.refresh_token else {
|
|
|
- return Err(OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- "no refresh_token given".into(),
|
|
|
- None,
|
|
|
- ));
|
|
|
- };
|
|
|
-
|
|
|
- let (access, refresh) = match rhelper.trade_refresh_token(&client, rtoken.as_str()) {
|
|
|
- Ok((a, r)) => (a, r),
|
|
|
- Err(e) => return Err(OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- format!("could not trade refresh token: {e}"),
|
|
|
- None,
|
|
|
- ))
|
|
|
- };
|
|
|
- Ok(tide::Response::builder(200)
|
|
|
- .content_type(tide::http::mime::JSON)
|
|
|
- .body(
|
|
|
- serde_json::to_value(api::TokenResponse {
|
|
|
- access_token: access.as_str(),
|
|
|
- token_type: "bearer",
|
|
|
- refresh_token: Some(refresh.as_str()),
|
|
|
- scope: None,
|
|
|
- })
|
|
|
- .unwrap(),
|
|
|
- )
|
|
|
- .build())
|
|
|
- } else {
|
|
|
- Err(OIDCError(
|
|
|
- OIDCErrorType::InvalidRequest,
|
|
|
- format!("unknown grant type {}", treq.grant_type),
|
|
|
- None,
|
|
|
- ))
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
async fn token(request: Request) -> tide::Result<tide::Response> {
|
|
|
- match do_token(request).await {
|
|
|
+ match token::do_token(request).await {
|
|
|
Ok(res) => Ok(res),
|
|
|
Err(e) => Ok(e.to_response()),
|
|
|
}
|
|
@@ -395,46 +98,6 @@ async fn jwks(request: Request) -> tide::Result<tide::Response> {
|
|
|
jwkset.keys.push(key.wrapped().into_jwk());
|
|
|
}
|
|
|
|
|
|
- /*let keyinfo =
|
|
|
- realm
|
|
|
- .keys
|
|
|
- .get()?
|
|
|
- .into_iter()
|
|
|
- .map(|key| match key::ParsedKey::parse_from(&key)? {
|
|
|
- key::ParsedKey::HMAC { key_id, hmty, .. } => Ok(serde_json::json!({
|
|
|
- "kid": key_id,
|
|
|
- "alg": match hmty {
|
|
|
- key::HMacType::Sha256 => "HS256",
|
|
|
- key::HMacType::Sha512 => "HS512",
|
|
|
- },
|
|
|
- "hmac": true,
|
|
|
- })),
|
|
|
- key::ParsedKey::Ed25519 { key_id, keypair } => Ok(serde_json::json!({
|
|
|
- "crv": "Ed25519",
|
|
|
- "kid": key_id,
|
|
|
- "kty": "OKP",
|
|
|
- "use": "sig",
|
|
|
-
|
|
|
- "x": base64::encode(keypair.public_key().as_ref()),
|
|
|
- })),
|
|
|
- key::ParsedKey::RSA { key_id, keypair } => {
|
|
|
- let pubkey = keypair.public_key();
|
|
|
- Ok(serde_json::json!({
|
|
|
- "alg": "RS256",
|
|
|
- "kid": key_id,
|
|
|
- "kty": "RSA",
|
|
|
- "use": "sig",
|
|
|
-
|
|
|
- "e": base64::encode(pubkey.exponent().big_endian_without_leading_zero()),
|
|
|
- "n": base64::encode(pubkey.modulus().big_endian_without_leading_zero()),
|
|
|
- }))
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- let jwks_response = serde_json::json!({
|
|
|
- "keys": keyinfo.collect::<Result<Vec<_>, UIDCError>>()?,
|
|
|
- });*/
|
|
|
-
|
|
|
Ok(tide::Response::builder(200)
|
|
|
.header(tide::http::headers::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
|
|
.content_type(tide::http::mime::JSON)
|