Totally new to Rust. I am trying to implement oauth authentication and I am using axum, with no success.. Here is my ugly code:
use axum::{
Json,
extract::Query,
extract::Extension,
http::StatusCode,
response::IntoResponse
};
use serde_json::{json, Value};
use hyper;
use hyper_tls::HttpsConnector;
use hyper::header;
use cookie::Cookie;
use serde::{ Deserialize, Serialize };
#[derive(Clone)]
pub struct GitHubOAuth2 {
client_id: String,
redirect_uri: String,
client_secret: String
}
#[derive(Serialize, Deserialize)]
pub struct CallbackAuthCode {
code: String
}
impl GitHubOAuth2 {
pub fn new(conf: String) -> GitHubOAuth2 {
let json_content : Value = serde_json::from_str(&conf).expect("Invalid configuration.");
GitHubOAuth2 {
client_id: json_content["github_oauth2"]["client_id"].as_str().unwrap().to_string(),
redirect_uri: json_content["github_oauth2"]["redirect_uri"].as_str().unwrap().to_string(),
client_secret: json_content["github_oauth2"]["client_secret"].as_str().unwrap().to_string()
}
}
}
pub async fn callback(Query(params): Query<CallbackAuthCode>, Extension(conf): Extension<GitHubOAuth2>) -> impl IntoResponse {
let params_code = ¶ms.code;
let mut get_token_url: String = String::from("https://www.github.com/login/oauth/access_token?client_id=");
get_token_url.push_str(&conf.client_id);
get_token_url.push_str("&redirect_uri=");
get_token_url.push_str(&conf.redirect_uri);
get_token_url.push_str("&client_secret=");
get_token_url.push_str(&conf.client_secret);
get_token_url.push_str("&code=");
get_token_url.push_str(¶ms_code);
println!("get_token_url: {}", get_token_url);
let https = HttpsConnector::new();
let client = hyper::Client::builder().build::<_, hyper::Body>(https);
let req = hyper::Request::builder()
.method(hyper::Method::POST)
.uri(get_token_url)
.header("Accept", "application/json")
.body(hyper::Body::empty()).unwrap();
match client.request(req).await {
Ok(resp) => {
println!("response: {}", resp.status());
let redirectUri : String = resp.headers().get("Location").unwrap().to_str().unwrap().to_string();
if resp.status() == 301 {
let redirectReq = hyper::Request::builder()
.method(hyper::Method::POST)
.uri(redirectUri)
.header("Accept", "application/json")
.body(hyper::Body::empty()).unwrap();
match client.request(redirectReq).await {
Ok(mut redirectResp) => {
let body = hyper::body::to_bytes(redirectResp.body_mut()).await.unwrap();
println!("{} {:?}", redirectResp.status(), body);
let body_as_json : Value = serde_json::from_slice(&body).unwrap();
let bearer_token = body_as_json["access_token"].as_str().unwrap().to_string();
let cookie = Cookie::build("hey", bearer_token).secure(true).http_only(true).finish();
return (
StatusCode::OK,
[(header::SET_COOKIE, &cookie.value())],
Json(json!({
"msg": "got that cookie"
}))
);
},
Err(mut redirect_e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
[(header::CONTENT_TYPE, &"application/json")],
Json(json!({
"error": redirect_e.to_string()
}))
);
}
}
} else {
return (
StatusCode::NOT_IMPLEMENTED,
[(header::CONTENT_TYPE, &"application/json")],
Json(json!({
"error": String::from("service replies with unexpected response.")
}))
);
}
},
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
[(header::CONTENT_TYPE, &"application/json")],
Json(json!({
"error": e.to_string()
}))
);
}
}
}
callback
function aims to implement the 'callback' phase in oauth2, so it picks the auth code and uses it to call github IdP to collect authorization token. No big deal. Problem is this code does not compile and I don't get why. Compiler says:
error[E0277]: the trait bound `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>): IntoResponse` is not satisfied
callback
function is attached to server as a get
handler.
I started from the axum basic examples and tried to build my monster one step at time but now I am stuck. What am I missing?
My Cargo.toml
:
[package]
name = "keymaster"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.5.16"
axum-macros = "0.2.3"
hyper = { version = "0.14.20", features = ["full"] }
tokio = { version = "1.21.2", features = ["full"] }
tower = "0.4.13"
serde_json = "1.0.85"
serde = "1.0.145"
jwt-simple = "0.10"
clap = { version = "4.0.19", features = ["derive"] }
hyper-tls = "0.5.0"
cookie = "0.17.0"
follow-redirects = "0.1.3"
http = "0.2.9"
[dependencies.uuid]
version = "1.2.1"
features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]
Removing &
from my &"application/json"
causes the compiler to report some more errors. The full log is here:
--> src/server/handlers/github_oauth2.rs:14:5
|
14 | use http::header::HeaderValue;
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
error[E0308]: mismatched types
--> src/server/handlers/github_oauth2.rs:92:41
|
92 | [(header::CONTENT_TYPE, "application/json")],
| ^^^^^^^^^^^^^^^^^^ expected `&str`, found `str`
|
= note: expected reference `&&str`
found reference `&'static str`
note: return type inferred to be `&&str` here
--> src/server/handlers/github_oauth2.rs:81:22
|
81 | return (
| ______________________^
82 | | StatusCode::OK,
83 | | [(header::SET_COOKIE, &cookie.value())],
84 | | Json(json!({
85 | | "msg": "got that cookie"
86 | | }))
87 | | );
| |_______________^
error[E0277]: the trait bound `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>): IntoResponse` is not satisfied
--> src/server/handlers/github_oauth2.rs:40:126
|
40 | pub async fn callback(Query(params): Query<CallbackAuthCode>, Extension(conf): Extension<GitHubOAuth2>) -> impl IntoResponse {
| ______________________________________________________________________________________________________________________________^
41 | | let params_code = ¶ms.code;
42 | |
43 | | let mut get_token_url: String = String::from("https://www.github.com/login/oauth/access_token?client_id=");
... |
118 | | }
119 | | }
| |_^ the trait `IntoResponse` is not implemented for `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>)`
|
= help: the following other types implement trait `IntoResponse`:
()
(Response<()>, R)
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
(Response<()>, T1, T2, T3, R)
(Response<()>, T1, T2, T3, T4, R)
(Response<()>, T1, T2, T3, T4, T5, R)
(Response<()>, T1, T2, T3, T4, T5, T6, R)
and 60 others
error[E0308]: mismatched types
--> src/server/handlers/github_oauth2.rs:102:35
|
102 | [(header::CONTENT_TYPE, "application/json")],
| ^^^^^^^^^^^^^^^^^^ expected `&str`, found `str`
|
= note: expected reference `&&str`
found reference `&'static str`
note: return type inferred to be `&&str` here
--> src/server/handlers/github_oauth2.rs:81:22
|
81 | return (
| ______________________^
82 | | StatusCode::OK,
83 | | [(header::SET_COOKIE, &cookie.value())],
84 | | Json(json!({
85 | | "msg": "got that cookie"
86 | | }))
87 | | );
| |_______________^
error[E0308]: mismatched types
--> src/server/handlers/github_oauth2.rs:112:33
|
112 | [(header::CONTENT_TYPE, "application/json")],
| ^^^^^^^^^^^^^^^^^^ expected `&str`, found `str`
|
= note: expected reference `&&str`
found reference `&'static str`
note: return type inferred to be `&&str` here
--> src/server/handlers/github_oauth2.rs:81:22
|
81 | return (
| ______________________^
82 | | StatusCode::OK,
83 | | [(header::SET_COOKIE, &cookie.value())],
84 | | Json(json!({
85 | | "msg": "got that cookie"
86 | | }))
87 | | );
| |_______________^
error[E0277]: the trait bound `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>): IntoResponse` is not satisfied
--> src/server/handlers/github_oauth2.rs:40:108
|
40 | pub async fn callback(Query(params): Query<CallbackAuthCode>, Extension(conf): Extension<GitHubOAuth2>) -> impl IntoResponse {
| ^^^^^^^^^^^^^^^^^ the trait `IntoResponse` is not implemented for `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>)`
|
= help: the following other types implement trait `IntoResponse`:
()
(Response<()>, R)
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
(Response<()>, T1, T2, T3, R)
(Response<()>, T1, T2, T3, T4, R)
(Response<()>, T1, T2, T3, T4, T5, R)
(Response<()>, T1, T2, T3, T4, T5, T6, R)
and 60 others
Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.
warning: `keymaster` (bin "keymaster") generated 1 warning
error: could not compile `keymaster` due to 5 previous errors; 1 warning emitted
The error seems clear? It's telling you that
(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>)
does not implement IntoResponse
.
Since this is a 3-uple and the first member is a StatusCode
, the closest impl would be
impl<R, T1> IntoResponse for (StatusCode, T1, R)
where
T1: IntoResponseParts,
R: IntoResponse,
There is an
impl<T> IntoResponse for Json<T> where T: Serialize
and serde_value::Value
implements Serialize
, which leaves the middle item:
[(HeaderName, &&str); 1]
This is an array of a (k, v) tuple so
impl<K, V, const N: usize> IntoResponse for [(K, V); N]
where
K: TryInto<HeaderName>,
<K as TryInto<HeaderName>>::Error: Display,
V: TryInto<HeaderValue>,
<V as TryInto<HeaderValue>>::Error: Display
is the only candidate. HeaderName
is trivially convertible to a HeaderName
, but what about &&str as TryInto<HeaderValue>
? Looking at impl TryFrom<> for HeaderValue
(which is the mirror and usually implemented trait) we can see:
impl<'a> TryFrom<&'a [u8]> for HeaderValue
impl<'a> TryFrom<&'a String> for HeaderValue
impl<'a> TryFrom<&'a str> for HeaderValue
impl TryFrom<String> for HeaderValue
impl TryFrom<Vec<u8, Global>> for HeaderValue
And indeed there is no implementation from &&str
, so &&str
can't be converted to a HeaderValue
.
Just remove the completely unnecessary referencing of you string literals, a string literal is already an &'static str
.