Open RobertoSnap opened 8 months ago
Hemm, it seem that its rather the oauth handle that is not compatible by the service im communicating with. Think i need to create my own middleware.
Thank you for the kind words.
At a high level it looks like you're using it correctly. In your update, are you saying the service implements something similar to oauth, but not oauth exactly?
If it's still oauth, but there's some support missing in this lib, happy to help.
Yea, exactly, i dont think what they require is oAuth. They require that i set a Basic auth from client and secret, and accept header to appication/json:
Like this
.header("accept", "application/json")
.content_type("application/x-www-form-urlencoded")
.basic_auth(&create_basic_auth(&self.client_id, &self.client_secret))
I dont know if this is general or specific in regards to other services and worth supporting. Im creating an extension to oauth and oauth flow now.
It went very smooth creating a modification of the oauth and oauth-flow (thanks to your awesome code).
Tell me if this is something general and i can see how its possible to create it as params in a PR.
flow
use std::io::ErrorKind;
use std::sync::RwLock;
use httpclient::InMemoryResponseExt;
use httpclient::{client, InMemoryBody, InMemoryResult, Result, Uri};
use serde::de;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::integrations::fiken::oauth::{FikenOAuth, TokenType};
use crate::utils::{
credentials::create_basic_auth,
random::{create_random_value_with_secret, verify_random_value_with_secret},
};
pub struct FikenOAuth2Flow {
pub client_id: String,
pub client_secret: String,
/// The endpoint to initialize the flow. (Step 1)
pub init_endpoint: String,
/// The endpoint to exchange the code for an access token. (Step 2)
pub exchange_endpoint: String,
/// The endpoint to refresh the access token.
pub refresh_endpoint: String,
pub redirect_uri: String,
}
#[derive(Debug, Serialize)]
pub(super) struct TokenEndpointParams<'a> {
pub client_id: &'a str,
pub redirect_uri: &'a str,
/// value should be "code". TODO to remove the field from the struct
pub response_type: &'static str,
pub access_type: &'static str,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct FikenTokenEndpointBody {
code: String,
redirect_uri: String,
grant_type: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExchangeResponse {
pub access_token: String,
pub expires_in: u64,
pub refresh_token: Option<String>,
pub scope: Option<String>,
pub token_type: String,
}
impl FikenOAuth2Flow {
pub fn create_authorization_url(&self) -> Uri {
let state = create_random_value_with_secret();
let params = TokenEndpointParams {
client_id: &self.client_id,
redirect_uri: &self.redirect_uri,
response_type: "code",
access_type: "authorization_code",
};
let params = serde_qs::to_string(¶ms).unwrap();
let endpoint = self.init_endpoint.as_str();
let uri = format!("{endpoint}?{params}");
uri.parse().unwrap()
}
pub async fn exchange(&self, code: String, state: String) -> InMemoryResult<ExchangeResponse> {
if !verify_random_value_with_secret(state.as_str()) {
panic!("Invalid state");
}
let res = client()
.post(&self.exchange_endpoint)
.form(FikenTokenEndpointBody {
code: code,
redirect_uri: self.redirect_uri.clone(),
grant_type: "authorization_code".to_string(),
})
.header("accept", "application/json")
.content_type("application/x-www-form-urlencoded")
.basic_auth(&create_basic_auth(&self.client_id, &self.client_secret))
.await?;
Ok(res.json()?)
}
pub fn bearer_middleware(&self, access: String, refresh: String) -> FikenOAuth {
FikenOAuth {
refresh_endpoint: self.refresh_endpoint.clone(),
client_id: self.client_id.clone(),
client_secret: self.client_secret.clone(),
token_type: TokenType::Bearer,
access_token: RwLock::new(access),
refresh_token: refresh,
callback: None,
}
}
}
oauth
use std::fmt::{Debug, Formatter};
use std::sync::RwLock;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use httpclient::{
header, HeaderName, InMemoryRequest, Method, Middleware, Next, ProtocolResult, RequestBuilder,
Response,
};
use crate::utils::credentials::create_basic_auth;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TokenType {
Bearer,
#[serde(untagged)]
Other(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RefreshData {
pub access_token: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct RefreshResponse {
pub access_token: String,
pub expires_in: u64,
pub scope: Option<String>,
pub token_type: TokenType,
}
// const NO_OP: fn(RefreshData) = |_: RefreshData| {};
// fn(..) -> ()
// Fn()
// FnMut()
// FnOnce()
pub struct FikenOAuth {
// Configuration
pub refresh_endpoint: String,
pub client_id: String,
pub client_secret: String,
pub token_type: TokenType,
// State
pub access_token: RwLock<String>,
pub refresh_token: String,
pub callback: Option<Box<dyn Fn(RefreshData) + Send + Sync + 'static>>,
}
impl Debug for FikenOAuth {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OAuth2")
.field("refresh_endpoint", &self.refresh_endpoint)
.field("client_id", &self.client_id)
.field("client_secret", &self.client_secret)
.field("token_type", &self.token_type)
.field("access_token", &self.access_token)
.field("refresh_token", &self.refresh_token)
.field(
"callback",
&self.callback.as_ref().map(|_| "Fn(RefreshData)"),
)
.finish()
}
}
impl FikenOAuth {
pub fn callback(&mut self, data: impl Fn(RefreshData) + Send + Sync + 'static) {
self.callback = Some(Box::new(data));
}
fn authorize(&self, mut request: InMemoryRequest) -> InMemoryRequest {
let access_token = self.access_token.read().unwrap();
let access_token = access_token.as_str();
match &self.token_type {
TokenType::Bearer => {
request.headers_mut().insert(
header::AUTHORIZATION,
format!("Bearer {}", access_token).parse().unwrap(),
);
}
TokenType::Other(s) => {
request.headers_mut().insert(
s.parse::<HeaderName>().unwrap(),
access_token.parse().unwrap(),
);
}
}
request
}
}
#[async_trait]
impl Middleware for FikenOAuth {
async fn handle(&self, request: InMemoryRequest, next: Next<'_>) -> ProtocolResult<Response> {
let req = self.authorize(request);
let res = next.run(req.clone().into()).await;
if !matches!(&res, Ok(resp) if resp.status().as_u16() == 401) {
// if we didn't get a 401, proceed as normal
return res;
}
let refresh_req = RequestBuilder::new(
next.client,
Method::POST,
self.refresh_endpoint.parse().unwrap(),
)
.form(RefreshRequest {
grant_type: "refresh_token",
refresh_token: &self.refresh_token,
})
.header("accept", "application/json")
.content_type("application/x-www-form-urlencoded")
.basic_auth(&create_basic_auth(&self.client_id, &self.client_secret))
.build();
let res = next.run(refresh_req).await?;
let (_, body) = res.into_parts();
let body = body.into_memory().await?;
let data: RefreshResponse = body.json()?;
{
let mut access_token = self.access_token.write().unwrap();
*access_token = data.access_token.clone();
}
if let Some(callback) = self.callback.as_ref() {
callback(RefreshData {
access_token: data.access_token,
});
}
// // reauthorize the request with the newly set access token. it will overwrite the previously set headers
let req = self.authorize(req);
next.run(req.clone().into()).await
}
}
#[derive(Debug, Serialize)]
struct RefreshRequest<'a> {
pub grant_type: &'static str,
pub refresh_token: &'a str,
}
``
Thank you again!
The main difference is where the client_id & client_secret are passed in, correct? Do you have a sense of whether that's a common pattern for implementing OAuth? I've only encountered the one that is currently implemented in the library.
If it's common enough, it seems perfectly reasonable to provide both a FormOAuth2
middleware and a BasicAuthOAuth2
middleware (names are examples) in this lib, and a PR would be welcome.
First, thank you for a fantastic create. Discovered it through libninja.
So I have used Libninja to create an API for a service here in Norway.
Then in a axum endpoint i try and create a client and the middleware.
I then run a request and it works as expected until the access token expires.
After the access token expires, i get this panic:
But why is the request not first run through the OAuth middleware?