|
@@ -1,10 +1,12 @@
|
|
|
-use crate::{key, schema, token, UIDCError};
|
|
|
+use crate::{config, key, schema, token, UIDCError};
|
|
|
use microrm::prelude::*;
|
|
|
use ring::signature::KeyPair;
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
use super::session::SessionHelper;
|
|
|
|
|
|
+mod api;
|
|
|
+
|
|
|
type Request = tide::Request<super::ServerStateWrapper>;
|
|
|
|
|
|
#[derive(serde::Serialize)]
|
|
@@ -42,6 +44,16 @@ impl<'a> OIDCError<'a> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+impl<'a> From<microrm::Error> for OIDCError<'a> {
|
|
|
+ fn from(value: microrm::Error) -> Self {
|
|
|
+ Self(
|
|
|
+ OIDCErrorType::ServerError,
|
|
|
+ format!("Internal database error: {value}"),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
#[derive(Deserialize)]
|
|
|
struct AuthorizeQueryParams {
|
|
|
response_type: String,
|
|
@@ -50,21 +62,83 @@ struct AuthorizeQueryParams {
|
|
|
scope: Option<String>,
|
|
|
}
|
|
|
|
|
|
-fn do_code_authorize(
|
|
|
- request: Request,
|
|
|
- qp: AuthorizeQueryParams,
|
|
|
- state: Option<&str>,
|
|
|
- client: microrm::Stored<schema::Client>,
|
|
|
-) -> Result<tide::Response, OIDCError> {
|
|
|
- todo!()
|
|
|
+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_token_authorize(
|
|
|
- request: Request,
|
|
|
- qp: AuthorizeQueryParams,
|
|
|
- state: Option<&str>,
|
|
|
- client: microrm::Stored<schema::Client>,
|
|
|
-) -> Result<tide::Response, OIDCError> {
|
|
|
+fn do_token_authorize<'l, 's>(
|
|
|
+ config: &config::Config,
|
|
|
+ realm: &schema::Realm,
|
|
|
+ client: &schema::Client,
|
|
|
+ user: &schema::User,
|
|
|
+ scopes: impl Iterator<Item = &'l str>,
|
|
|
+ state: Option<&'s str>,
|
|
|
+) -> Result<tide::Response, OIDCError<'s>> {
|
|
|
+ let token = token::generate_access_token(config, &realm, client, user, scopes.into_iter());
|
|
|
+
|
|
|
+ // TODO: use api::TokenResponse here
|
|
|
+ let response_body = serde_json::json!({
|
|
|
+ "token": token.map_err(|e| OIDCError(OIDCErrorType::ServerError, format!("error while generating token: {:?}", e), state))?,
|
|
|
+ });
|
|
|
+ Ok(tide::Response::builder(200)
|
|
|
+ .content_type(tide::http::mime::JSON)
|
|
|
+ .body(response_body)
|
|
|
+ .build())
|
|
|
+}
|
|
|
+
|
|
|
+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(
|
|
@@ -75,7 +149,7 @@ fn do_token_authorize(
|
|
|
})?;
|
|
|
|
|
|
let make_redirect = || {
|
|
|
- let mut login_url = request.url().join("../session/login").unwrap();
|
|
|
+ let mut login_url = request.url().join("../v1/session/login").unwrap();
|
|
|
login_url
|
|
|
.query_pairs_mut()
|
|
|
.clear()
|
|
@@ -83,6 +157,12 @@ fn do_token_authorize(
|
|
|
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));
|
|
@@ -105,46 +185,6 @@ fn do_token_authorize(
|
|
|
));
|
|
|
};
|
|
|
|
|
|
- let scopes = qp
|
|
|
- .scope
|
|
|
- .as_ref()
|
|
|
- .map(|slist| slist.as_str())
|
|
|
- .unwrap_or("")
|
|
|
- .split_whitespace();
|
|
|
-
|
|
|
- // TODO: check that redirect URI matches
|
|
|
-
|
|
|
- let token = token::generate_auth_token(
|
|
|
- &request.state().core.config,
|
|
|
- &realm,
|
|
|
- client.as_ref(),
|
|
|
- user.as_ref(),
|
|
|
- scopes,
|
|
|
- );
|
|
|
-
|
|
|
- let response_body = serde_json::json!({
|
|
|
- "token": token.map_err(|e| OIDCError(OIDCErrorType::ServerError, format!("error while generating token: {:?}", e), state))?,
|
|
|
- });
|
|
|
- Ok(tide::Response::builder(200)
|
|
|
- .content_type(tide::http::mime::JSON)
|
|
|
- .body(response_body)
|
|
|
- .build())
|
|
|
-}
|
|
|
-
|
|
|
-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 qp: AuthorizeQueryParams = request
|
|
|
- .query()
|
|
|
- .map_err(|x| OIDCError(OIDCErrorType::InvalidRequest, x.to_string(), state))?;
|
|
|
-
|
|
|
// verify the realm and client_id and redirect_uri
|
|
|
|
|
|
let client = realm
|
|
@@ -162,10 +202,34 @@ fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response,
|
|
|
)
|
|
|
})?;
|
|
|
|
|
|
+ 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, qp, state, client)
|
|
|
+ do_code_authorize(
|
|
|
+ &request.state().core.config,
|
|
|
+ &realm,
|
|
|
+ &client,
|
|
|
+ &user,
|
|
|
+ scopes,
|
|
|
+ qp.redirect_uri,
|
|
|
+ state,
|
|
|
+ )
|
|
|
} else if qp.response_type == "token" {
|
|
|
- do_token_authorize(request, qp, state, client)
|
|
|
+ do_token_authorize(
|
|
|
+ &request.state().core.config,
|
|
|
+ &realm,
|
|
|
+ &client,
|
|
|
+ &user,
|
|
|
+ scopes,
|
|
|
+ state,
|
|
|
+ )
|
|
|
} else {
|
|
|
Err(OIDCError(
|
|
|
OIDCErrorType::UnsupportedResponseType,
|
|
@@ -188,24 +252,111 @@ async fn authorize(request: Request) -> tide::Result<tide::Response> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-#[derive(Deserialize)]
|
|
|
-struct TokenRequestBody {
|
|
|
- grant_type: String,
|
|
|
- refresh_token: Option<String>,
|
|
|
- scope: Option<String>,
|
|
|
- redirect_uri: Option<String>,
|
|
|
- code: Option<String>,
|
|
|
- client_id: Option<String>,
|
|
|
+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))?;
|
|
|
|
|
|
- // direct grant
|
|
|
- username: Option<String>,
|
|
|
- password: Option<String>,
|
|
|
+ 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,
|
|
|
+ ))?;
|
|
|
+
|
|
|
+ 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::InvalidRequest,
|
|
|
+ "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 = token::generate_access_token(
|
|
|
+ &request.state().core.config,
|
|
|
+ &realm,
|
|
|
+ &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" {
|
|
|
+ todo!()
|
|
|
+ } else {
|
|
|
+ Err(OIDCError(
|
|
|
+ OIDCErrorType::InvalidRequest,
|
|
|
+ format!("unknown grant type {}", treq.grant_type),
|
|
|
+ None,
|
|
|
+ ))
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-async fn token(mut request: Request) -> tide::Result<tide::Response> {
|
|
|
- let body: Result<TokenRequestBody, _> = request.body_form().await;
|
|
|
- // let body : TokenRequestBody = request.body_form();
|
|
|
- todo!()
|
|
|
+async fn token(request: Request) -> tide::Result<tide::Response> {
|
|
|
+ match do_token(request).await {
|
|
|
+ Ok(res) => Ok(res),
|
|
|
+ Err(e) => Ok(e.to_response()),
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const AUTHORIZE_PATH: &'static str = "oidc/authorize";
|
|
@@ -213,9 +364,8 @@ const TOKEN_PATH: &'static str = "oidc/token";
|
|
|
const JWKS_PATH: &'static str = "oidc/jwks";
|
|
|
|
|
|
async fn jwks(request: Request) -> tide::Result<tide::Response> {
|
|
|
- let shelper = super::session::SessionHelper::new(&request);
|
|
|
+ let shelper = SessionHelper::new(&request);
|
|
|
let realm = shelper.get_realm()?;
|
|
|
- // let rkeys = key::RealmKeys::new(realm.wrapped());
|
|
|
|
|
|
let keyinfo =
|
|
|
realm
|