fix: better scope handling
This commit is contained in:
parent
a7f6c28e0d
commit
81b249d341
10 changed files with 61 additions and 30 deletions
30
TODO.md
30
TODO.md
|
@ -2,12 +2,29 @@
|
||||||
|
|
||||||
- [x] Login form
|
- [x] Login form
|
||||||
- [x] Register form
|
- [x] Register form
|
||||||
- [x] Generate JWT
|
- [x] Redirect to login form if unauthenticated
|
||||||
- [ ] Redirect to login form if unauthenticated
|
|
||||||
- [x] Authorize form
|
|
||||||
- [x] Verify authorize
|
|
||||||
- [x] Upload picture
|
- [x] Upload picture
|
||||||
- [x] Get access token
|
|
||||||
|
- OAuth2
|
||||||
|
- [x] Authorize form
|
||||||
|
- [x] Verify authorize
|
||||||
|
- [x] Get access token
|
||||||
|
|
||||||
|
- [x] Support OpenID to use with demo client [oauth2c](https://github.com/cloudentity/oauth2c)
|
||||||
|
- .well-known/openid-configuration
|
||||||
|
|
||||||
|
|
||||||
|
- [ ] i18n strings in the http website.
|
||||||
|
|
||||||
|
- [ ] App config
|
||||||
|
- Add app logo (URI?)
|
||||||
|
|
||||||
|
- [ ] Public endpoint to get user avatar by id
|
||||||
|
- [ ] Rework avatar upload to limit size and process the image?
|
||||||
|
|
||||||
|
- [ ] Authorize form
|
||||||
|
- Show details about permissions
|
||||||
|
- Show app logo
|
||||||
|
|
||||||
- [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
|
- [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
|
||||||
|
|
||||||
|
@ -15,6 +32,3 @@
|
||||||
- [ ] Add admin panel via API
|
- [ ] Add admin panel via API
|
||||||
- [ ] Add admin CLI
|
- [ ] Add admin CLI
|
||||||
|
|
||||||
- [ ] Support OpenID to use with demo client [oauth2c](https://github.com/cloudentity/oauth2c)
|
|
||||||
|
|
||||||
- .well-known/openid-configuration
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ curl -v http://localhost:8085/authorize \
|
||||||
-d client_id="a1785786-8be1-443c-9a6f-35feed703609" \
|
-d client_id="a1785786-8be1-443c-9a6f-35feed703609" \
|
||||||
-d response_type="code" \
|
-d response_type="code" \
|
||||||
-d redirect_uri="http://localhost:9090/authorize" \
|
-d redirect_uri="http://localhost:9090/authorize" \
|
||||||
-d scope="read_basics" \
|
-d scope="user_read_basic" \
|
||||||
-d state="qxYAfk4kf6pbZkms78jM"
|
-d state="qxYAfk4kf6pbZkms78jM"
|
||||||
|
|
||||||
code="$(cat tmp/headers.txt | grep -i "location" | awk -F ": " '{print $2}' | trurl -f - -g "{query:code}")"
|
code="$(cat tmp/headers.txt | grep -i "location" | awk -F ": " '{print $2}' | trurl -f - -g "{query:code}")"
|
||||||
|
|
|
@ -7,4 +7,4 @@ oauth2c http://localhost:8085 \
|
||||||
--response-mode query \
|
--response-mode query \
|
||||||
--grant-type authorization_code \
|
--grant-type authorization_code \
|
||||||
--auth-method client_secret_basic \
|
--auth-method client_secret_basic \
|
||||||
--scopes "read_user_basic"
|
--scopes "user_read_basic"
|
||||||
|
|
|
@ -76,9 +76,10 @@ pub async fn get_access_token(
|
||||||
// 3. Generate JWT for oauth2 client user session
|
// 3. Generate JWT for oauth2 client user session
|
||||||
let jwt = create_token(
|
let jwt = create_token(
|
||||||
&app_state.secrets,
|
&app_state.secrets,
|
||||||
AppUserTokenClaims::from_client_user_id(
|
AppUserTokenClaims::new(
|
||||||
&app_client_session.client_id,
|
&app_client_session.client_id,
|
||||||
&authorization.user_id
|
&authorization.user_id,
|
||||||
|
authorization.scopes.to_vec()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// 4. return JWT
|
// 4. return JWT
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use axum::{extract::State, response::IntoResponse, Json};
|
use axum::{extract::State, response::IntoResponse, Json};
|
||||||
use fully_pub::fully_pub;
|
use fully_pub::fully_pub;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::server::AppState;
|
use crate::{models::authorization::AuthorizationScope, server::AppState};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[fully_pub]
|
#[fully_pub]
|
||||||
|
@ -25,7 +26,7 @@ pub async fn get_well_known_openid_configuration(
|
||||||
authorization_endpoint: format!("{}/authorize", base_url),
|
authorization_endpoint: format!("{}/authorize", base_url),
|
||||||
token_endpoint: format!("{}/api/token", base_url),
|
token_endpoint: format!("{}/api/token", base_url),
|
||||||
userinfo_endpoint: format!("{}/api/user", base_url),
|
userinfo_endpoint: format!("{}/api/user", base_url),
|
||||||
scopes_supported: vec!["read_user_basic".into()],
|
scopes_supported: AuthorizationScope::iter().map(|v| v.to_string()).collect(),
|
||||||
response_types_supported: vec!["code".into()],
|
response_types_supported: vec!["code".into()],
|
||||||
token_endpoint_auth_methods_supported: vec!["client_secret_basic".into()],
|
token_endpoint_auth_methods_supported: vec!["client_secret_basic".into()],
|
||||||
})
|
})
|
||||||
|
|
|
@ -87,13 +87,23 @@ pub async fn authorize_form(
|
||||||
).into_response();
|
).into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 1.4. Parse and validate scopes
|
||||||
|
let scopes = match parse_scope(&authorization_params.scope) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_err) => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Html("Invalid scopes. Scopes must be space-delimited and snake_case.")
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
// 2. Check if the app is already authorized
|
// 2. Check if the app is already authorized
|
||||||
let authorizations_res = sqlx::query_as::<_, Authorization>(
|
let authorizations_res = sqlx::query_as::<_, Authorization>(
|
||||||
"SELECT * FROM authorizations WHERE user_id = $1 AND client_id = $2"
|
"SELECT * FROM authorizations WHERE user_id = $1 AND client_id = $2 AND scopes = $3"
|
||||||
)
|
)
|
||||||
.bind(&token_claims.sub)
|
.bind(&token_claims.sub)
|
||||||
.bind(&authorization_params.client_id)
|
.bind(&authorization_params.client_id)
|
||||||
|
.bind(sqlx::types::Json(&scopes))
|
||||||
.fetch_one(&app_state.db)
|
.fetch_one(&app_state.db)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -160,17 +170,17 @@ pub async fn perform_authorize(
|
||||||
).into_response();
|
).into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// parse scope again
|
// 1.2. Parse and validate scope to use in DB
|
||||||
let scopes = match parse_scope(&authorize_form.scope) {
|
let scopes = match parse_scope(&authorize_form.scope) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(err) => {
|
Err(_err) => {
|
||||||
return (
|
return (
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
Html(format!("Invalid scope: {}", err))
|
Html("Invalid scopes.")
|
||||||
).into_response();
|
).into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 2. Create an authorizaton code
|
// 2. Create an authorization code
|
||||||
let authorization_code = get_random_alphanumerical(32);
|
let authorization_code = get_random_alphanumerical(32);
|
||||||
|
|
||||||
let authorization = Authorization {
|
let authorization = Authorization {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use axum::{body::Bytes, extract::State, response::IntoResponse, Extension};
|
use axum::{body::Bytes, extract::State, response::IntoResponse, Extension};
|
||||||
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
|
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
|
||||||
use fully_pub::fully_pub;
|
use fully_pub::fully_pub;
|
||||||
|
use log::error;
|
||||||
use minijinja::context;
|
use minijinja::context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -99,11 +100,11 @@ pub async fn me_perform_update_details(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
dbg!(&err);
|
error!("Cannot update user details. {}", err);
|
||||||
renderer.render(
|
renderer.render(
|
||||||
template_path,
|
template_path,
|
||||||
context!(
|
context!(
|
||||||
error => Some("Cannot update user details".to_string()),
|
error => Some("Cannot update user details.".to_string()),
|
||||||
user => user_res
|
user => user_res
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,17 +2,17 @@ use fully_pub::fully_pub;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
use strum_macros::{Display, EnumString};
|
use strum_macros::{Display, EnumIter, EnumString};
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, Serialize, Deserialize, PartialEq,
|
Clone, Debug, Serialize, Deserialize, PartialEq,
|
||||||
sqlx::Type,
|
sqlx::Type,
|
||||||
Display, EnumString
|
Display, EnumString, EnumIter
|
||||||
)]
|
)]
|
||||||
#[strum(serialize_all = "lowercase")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
#[fully_pub]
|
pub enum AuthorizationScope {
|
||||||
enum AuthorizationScope {
|
UserReadBasic,
|
||||||
ReadBasics
|
UserReadRoles
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug)]
|
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug)]
|
||||||
|
|
|
@ -2,6 +2,8 @@ use fully_pub::fully_pub;
|
||||||
use jsonwebtoken::get_current_timestamp;
|
use jsonwebtoken::get_current_timestamp;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::authorization::AuthorizationScope;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[fully_pub]
|
#[fully_pub]
|
||||||
struct UserTokenClaims {
|
struct UserTokenClaims {
|
||||||
|
@ -30,6 +32,7 @@ struct AppUserTokenClaims {
|
||||||
/// combined subject
|
/// combined subject
|
||||||
client_id: String,
|
client_id: String,
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
scopes: Vec<AuthorizationScope>,
|
||||||
/// token expiration
|
/// token expiration
|
||||||
exp: u64,
|
exp: u64,
|
||||||
/// token issuer
|
/// token issuer
|
||||||
|
@ -37,10 +40,11 @@ struct AppUserTokenClaims {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppUserTokenClaims {
|
impl AppUserTokenClaims {
|
||||||
pub fn from_client_user_id(client_id: &str, user_id: &str) -> Self {
|
pub fn new(client_id: &str, user_id: &str, scopes: Vec<AuthorizationScope>) -> Self {
|
||||||
AppUserTokenClaims {
|
AppUserTokenClaims {
|
||||||
client_id: client_id.into(),
|
client_id: client_id.into(),
|
||||||
user_id: user_id.into(),
|
user_id: user_id.into(),
|
||||||
|
scopes,
|
||||||
exp: get_current_timestamp() + 86_000,
|
exp: get_current_timestamp() + 86_000,
|
||||||
iss: "Minauth".into()
|
iss: "Minauth".into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>App name: {{ app.name }}</li>
|
<li>App name: {{ app.name }}</li>
|
||||||
<li>App description: <i>{{ app.description }}</i></li>
|
<li>App description: <i>{{ app.description }}</i></li>
|
||||||
<li>Permisions: {{ authorization_params.scope }}</li>
|
<li>Permissions: {{ authorization_params.scope }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<input type="hidden" name="client_id" value="{{ authorization_params.client_id }}" />
|
<input type="hidden" name="client_id" value="{{ authorization_params.client_id }}" />
|
||||||
<input type="hidden" name="scope" value="{{ authorization_params.scope }}" />
|
<input type="hidden" name="scope" value="{{ authorization_params.scope }}" />
|
||||||
|
|
Loading…
Reference in a new issue