mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-18 20:06:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64189263ba | ||
|
|
14b8333183 |
@@ -0,0 +1 @@
|
|||||||
|
Added support for the OAuth2 device authorization flow. Contributed by @ginger
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use axum::{Form, Json, extract::State, response::IntoResponse};
|
||||||
|
use http::StatusCode;
|
||||||
|
use service::oauth::grant::DeviceCodeRequest;
|
||||||
|
|
||||||
|
pub(crate) async fn device_authorization_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
Form(request): Form<DeviceCodeRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match services.oauth.request_device_code(request).await {
|
||||||
|
| Ok(response) => Ok(Json(response)),
|
||||||
|
| Err(err) => Err((StatusCode::BAD_REQUEST, Json(err))),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
pub(crate) use server_metadata::*;
|
pub(crate) use server_metadata::*;
|
||||||
|
|
||||||
|
mod device;
|
||||||
mod register_client;
|
mod register_client;
|
||||||
mod server_metadata;
|
mod server_metadata;
|
||||||
mod token;
|
mod token;
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
const CLIENT_REGISTER_PATH: &str = "client/register";
|
const CLIENT_REGISTER_PATH: &str = "client/register";
|
||||||
const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
||||||
const TOKEN_PATH: &str = "grant/token";
|
const TOKEN_PATH: &str = "grant/token";
|
||||||
|
const DEVICE_AUTHORIZATION_PATH: &str = "device";
|
||||||
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
||||||
|
|
||||||
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
||||||
@@ -53,4 +55,5 @@ fn oauth_router() -> Router<crate::State> {
|
|||||||
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
|
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
|
||||||
.route(concat!("/", TOKEN_PATH), post(token::token_route))
|
.route(concat!("/", TOKEN_PATH), post(token::token_route))
|
||||||
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
|
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
|
||||||
|
.route(concat!("/", DEVICE_AUTHORIZATION_PATH), post(device::device_authorization_route))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Ruma,
|
Ruma,
|
||||||
client::oauth::{
|
client::oauth::{
|
||||||
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH,
|
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, DEVICE_AUTHORIZATION_PATH,
|
||||||
TOKEN_REVOKE_PATH,
|
JWKS_URI_PATH, TOKEN_PATH, TOKEN_REVOKE_PATH,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +49,8 @@ pub(crate) async fn authorization_server_metadata(services: &Services) -> Value
|
|||||||
],
|
],
|
||||||
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
|
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
|
||||||
"code_challenge_methods_supported": ["S256"],
|
"code_challenge_methods_supported": ["S256"],
|
||||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
"device_authorization_endpoint": endpoint_base.join(DEVICE_AUTHORIZATION_PATH).unwrap(),
|
||||||
|
"grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
|
||||||
"issuer": services.config.get_client_domain(),
|
"issuer": services.config.get_client_domain(),
|
||||||
"jwks_uri": endpoint_base.join(JWKS_URI_PATH).unwrap(),
|
"jwks_uri": endpoint_base.join(JWKS_URI_PATH).unwrap(),
|
||||||
"prompt_values_supported": ["create"],
|
"prompt_values_supported": ["create"],
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ pub enum ApplicationType {
|
|||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum GrantType {
|
pub enum GrantType {
|
||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
|
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
|
||||||
|
DeviceCode,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-17
@@ -13,6 +13,7 @@
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::client_metadata::ResponseType;
|
use super::client_metadata::ResponseType;
|
||||||
|
use crate::oauth::client_metadata::GrantType;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct AuthorizationCodeQuery {
|
pub struct AuthorizationCodeQuery {
|
||||||
@@ -29,6 +30,33 @@ pub struct AuthorizationCodeQuery {
|
|||||||
pub prompt: Option<Prompt>,
|
pub prompt: Option<Prompt>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct AuthorizationCodeResponse {
|
||||||
|
pub state: String,
|
||||||
|
pub code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct DeviceCodeRequest {
|
||||||
|
pub client_id: String,
|
||||||
|
pub scope: RawScopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct DeviceCodeResponse {
|
||||||
|
pub device_code: String,
|
||||||
|
pub user_code: String,
|
||||||
|
pub verification_uri: Url,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub verification_uri_complete: Option<Url>,
|
||||||
|
pub expires_in: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct DeviceCodeVerifyQuery {
|
||||||
|
pub user_code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@@ -126,19 +154,29 @@ pub struct OAuthError {
|
|||||||
|
|
||||||
impl OAuthError {
|
impl OAuthError {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn invalid_request(error_description: &'static str) -> Self {
|
pub fn new(error: ErrorCode, error_description: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
error: ErrorCode::InvalidRequest,
|
error,
|
||||||
|
error_description: Cow::Owned(error_description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new_static(error: ErrorCode, error_description: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
error,
|
||||||
error_description: Cow::Borrowed(error_description),
|
error_description: Cow::Borrowed(error_description),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn invalid_request(error_description: &'static str) -> Self {
|
||||||
|
Self::new_static(ErrorCode::InvalidRequest, error_description)
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn invalid_grant(error_description: &'static str) -> Self {
|
pub const fn invalid_grant(error_description: &'static str) -> Self {
|
||||||
Self {
|
Self::new_static(ErrorCode::InvalidGrant, error_description)
|
||||||
error: ErrorCode::InvalidGrant,
|
|
||||||
error_description: Cow::Borrowed(error_description),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,36 +195,43 @@ pub enum ErrorCode {
|
|||||||
AccessDenied,
|
AccessDenied,
|
||||||
InvalidScope,
|
InvalidScope,
|
||||||
InvalidGrant,
|
InvalidGrant,
|
||||||
|
InvalidClient,
|
||||||
InvalidClientMetadata,
|
InvalidClientMetadata,
|
||||||
|
AuthorizationPending,
|
||||||
|
ExpiredToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct AuthorizationCodeResponse {
|
pub struct TokenRequest {
|
||||||
pub state: String,
|
pub client_id: String,
|
||||||
pub code: String,
|
#[serde(flatten)]
|
||||||
|
pub request: TokenRequestType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(tag = "grant_type", rename_all = "snake_case")]
|
#[serde(tag = "grant_type", rename_all = "snake_case")]
|
||||||
pub enum TokenRequest {
|
pub enum TokenRequestType {
|
||||||
AuthorizationCode {
|
AuthorizationCode {
|
||||||
code: String,
|
code: String,
|
||||||
redirect_uri: Url,
|
redirect_uri: Url,
|
||||||
client_id: String,
|
|
||||||
code_verifier: String,
|
code_verifier: String,
|
||||||
},
|
},
|
||||||
|
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
|
||||||
|
DeviceCode {
|
||||||
|
device_code: String,
|
||||||
|
},
|
||||||
RefreshToken {
|
RefreshToken {
|
||||||
client_id: String,
|
|
||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TokenRequest {
|
impl TokenRequestType {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn client_id(&self) -> &str {
|
pub fn grant_type(&self) -> GrantType {
|
||||||
match self {
|
match self {
|
||||||
| Self::AuthorizationCode { client_id, .. }
|
| Self::AuthorizationCode { .. } => GrantType::AuthorizationCode,
|
||||||
| Self::RefreshToken { client_id, .. } => client_id,
|
| Self::DeviceCode { .. } => GrantType::DeviceCode,
|
||||||
|
| Self::RefreshToken { .. } => GrantType::RefreshToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+226
-30
@@ -12,17 +12,19 @@
|
|||||||
use database::{Deserialized, Json, Map};
|
use database::{Deserialized, Json, Map};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lru_cache::LruCache;
|
use lru_cache::LruCache;
|
||||||
|
use rand::distr::{Distribution, slice::Choose};
|
||||||
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
|
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Dep,
|
Dep, config,
|
||||||
oauth::{
|
oauth::{
|
||||||
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
||||||
grant::{
|
grant::{
|
||||||
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod, ErrorCode,
|
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod,
|
||||||
OAuthError, ResponseMode, Scope, TokenRequest, TokenResponse, TokenType,
|
DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, ResponseMode, Scope,
|
||||||
|
TokenRequest, TokenRequestType, TokenResponse, TokenType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
users,
|
users,
|
||||||
@@ -35,7 +37,8 @@ pub struct Service {
|
|||||||
services: Services,
|
services: Services,
|
||||||
db: Data,
|
db: Data,
|
||||||
tickets: Mutex<HashMap<String, HashMap<OAuthTicket, SystemTime>>>,
|
tickets: Mutex<HashMap<String, HashMap<OAuthTicket, SystemTime>>>,
|
||||||
pending_code_grants: tokio::sync::Mutex<LruCache<String, PendingCodeGrant>>,
|
pending_auth_code_grants: tokio::sync::Mutex<LruCache<String, PendingAuthCodeGrant>>,
|
||||||
|
pending_device_code_grants: tokio::sync::Mutex<LruCache<String, PendingDeviceCodeGrant>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
@@ -46,6 +49,7 @@ struct Data {
|
|||||||
|
|
||||||
struct Services {
|
struct Services {
|
||||||
users: Dep<users::Service>,
|
users: Dep<users::Service>,
|
||||||
|
config: Dep<config::Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
@@ -62,7 +66,7 @@ struct RefreshTokenInfo {
|
|||||||
device_id: OwnedDeviceId,
|
device_id: OwnedDeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingCodeGrant {
|
struct PendingAuthCodeGrant {
|
||||||
authorizing_user: OwnedUserId,
|
authorizing_user: OwnedUserId,
|
||||||
requested_scopes: BTreeSet<Scope>,
|
requested_scopes: BTreeSet<Scope>,
|
||||||
client_name: Option<String>,
|
client_name: Option<String>,
|
||||||
@@ -72,12 +76,8 @@ struct PendingCodeGrant {
|
|||||||
requested_at: SystemTime,
|
requested_at: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PendingCodeGrant {
|
impl PendingAuthCodeGrant {
|
||||||
const MAX_AGE: Duration = Duration::from_mins(1);
|
const MAX_AGE: Duration = Duration::from_mins(1);
|
||||||
const RANDOM_CODE_LENGTH: usize = 32;
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn generate_code() -> String { utils::random_string(Self::RANDOM_CODE_LENGTH) }
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
||||||
@@ -90,6 +90,43 @@ pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PendingDeviceCodeGrant {
|
||||||
|
state: DeviceCodeGrantState,
|
||||||
|
requested_scopes: BTreeSet<Scope>,
|
||||||
|
client_name: Option<String>,
|
||||||
|
client_id: String,
|
||||||
|
requested_at: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeviceCodeGrantState {
|
||||||
|
Unverified {
|
||||||
|
user_code: String,
|
||||||
|
},
|
||||||
|
Verified {
|
||||||
|
authorizing_user: OwnedUserId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PendingDeviceCodeGrant {
|
||||||
|
const MAX_AGE: Duration = Duration::from_mins(1);
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
||||||
|
let now = SystemTime::now();
|
||||||
|
|
||||||
|
self.client_id == client_id
|
||||||
|
&& now
|
||||||
|
.duration_since(self.requested_at)
|
||||||
|
.is_ok_and(|age| age < Self::MAX_AGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DeviceCodeGrantInfo {
|
||||||
|
pub device_code: String,
|
||||||
|
pub client_metadata: ClientMetadata,
|
||||||
|
pub requested_scopes: BTreeSet<Scope>,
|
||||||
|
}
|
||||||
|
|
||||||
/// A time-limited grant for a client to perform some sensitive action.
|
/// A time-limited grant for a client to perform some sensitive action.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum OAuthTicket {
|
pub enum OAuthTicket {
|
||||||
@@ -112,6 +149,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
services: Services {
|
services: Services {
|
||||||
users: args.depend::<users::Service>("users"),
|
users: args.depend::<users::Service>("users"),
|
||||||
|
config: args.depend::<config::Service>("config"),
|
||||||
},
|
},
|
||||||
db: Data {
|
db: Data {
|
||||||
clientid_clientmetadata: args.db["clientid_clientmetadata"].clone(),
|
clientid_clientmetadata: args.db["clientid_clientmetadata"].clone(),
|
||||||
@@ -119,8 +157,11 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
refreshtoken_refreshtokeninfo: args.db["refreshtoken_refreshtokeninfo"].clone(),
|
refreshtoken_refreshtokeninfo: args.db["refreshtoken_refreshtokeninfo"].clone(),
|
||||||
},
|
},
|
||||||
tickets: Mutex::default(),
|
tickets: Mutex::default(),
|
||||||
pending_code_grants: tokio::sync::Mutex::new(LruCache::new(
|
pending_auth_code_grants: tokio::sync::Mutex::new(LruCache::new(
|
||||||
Self::MAX_PENDING_CODE_GRANTS,
|
Self::MAX_PENDING_GRANTS,
|
||||||
|
)),
|
||||||
|
pending_device_code_grants: tokio::sync::Mutex::new(LruCache::new(
|
||||||
|
Self::MAX_PENDING_GRANTS,
|
||||||
)),
|
)),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -133,11 +174,21 @@ impl Service {
|
|||||||
// Maximum number of pending code grants which will be held in memory at once,
|
// Maximum number of pending code grants which will be held in memory at once,
|
||||||
// to prevent unbounded memory use if someone decides to repeatedly reload the
|
// to prevent unbounded memory use if someone decides to repeatedly reload the
|
||||||
// grant page.
|
// grant page.
|
||||||
const MAX_PENDING_CODE_GRANTS: usize = 100;
|
const MAX_PENDING_GRANTS: usize = 100;
|
||||||
const RANDOM_TOKEN_LENGTH: usize = 32;
|
const RANDOM_TOKEN_LENGTH: usize = 32;
|
||||||
|
const USER_CODE_CHARACTERS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
const USER_CODE_LENGTH: usize = 6;
|
||||||
|
|
||||||
fn generate_token() -> String { utils::random_string(Self::RANDOM_TOKEN_LENGTH) }
|
fn generate_token() -> String { utils::random_string(Self::RANDOM_TOKEN_LENGTH) }
|
||||||
|
|
||||||
|
fn generate_user_code() -> String {
|
||||||
|
Choose::new(Self::USER_CODE_CHARACTERS)
|
||||||
|
.unwrap()
|
||||||
|
.sample_iter(&mut rand::rng())
|
||||||
|
.take(Self::USER_CODE_LENGTH)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn register_client(&self, metadata: &ClientMetadata) -> Result<String, OAuthError> {
|
pub async fn register_client(&self, metadata: &ClientMetadata) -> Result<String, OAuthError> {
|
||||||
metadata.validate().map_err(|error| OAuthError {
|
metadata.validate().map_err(|error| OAuthError {
|
||||||
error: ErrorCode::InvalidClientMetadata,
|
error: ErrorCode::InvalidClientMetadata,
|
||||||
@@ -231,14 +282,14 @@ pub async fn request_authorization_code(
|
|||||||
| ResponseMode::Query => '?',
|
| ResponseMode::Query => '?',
|
||||||
};
|
};
|
||||||
|
|
||||||
let code = PendingCodeGrant::generate_code();
|
let code = Self::generate_token();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
client_id = &query.client_id,
|
client_id = &query.client_id,
|
||||||
client_name = &client_metadata.client_name,
|
client_name = &client_metadata.client_name,
|
||||||
?requested_scopes,
|
?requested_scopes,
|
||||||
?authorizing_user,
|
?authorizing_user,
|
||||||
"Issuing oauth authorization code"
|
"Issuing OAuth authorization code"
|
||||||
);
|
);
|
||||||
|
|
||||||
let redirect_uri = format!(
|
let redirect_uri = format!(
|
||||||
@@ -252,7 +303,7 @@ pub async fn request_authorization_code(
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let pending_grant = PendingCodeGrant {
|
let pending_grant = PendingAuthCodeGrant {
|
||||||
authorizing_user,
|
authorizing_user,
|
||||||
requested_scopes,
|
requested_scopes,
|
||||||
client_name: client_metadata.client_name,
|
client_name: client_metadata.client_name,
|
||||||
@@ -262,7 +313,7 @@ pub async fn request_authorization_code(
|
|||||||
requested_at: SystemTime::now(),
|
requested_at: SystemTime::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.pending_code_grants
|
self.pending_auth_code_grants
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.insert(code, pending_grant);
|
.insert(code, pending_grant);
|
||||||
@@ -270,15 +321,129 @@ pub async fn request_authorization_code(
|
|||||||
Ok(redirect_uri)
|
Ok(redirect_uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn request_device_code(
|
||||||
|
&self,
|
||||||
|
query: DeviceCodeRequest,
|
||||||
|
) -> Result<DeviceCodeResponse, OAuthError> {
|
||||||
|
let Some(client_metadata) = self.get_client_metadata(&query.client_id).await else {
|
||||||
|
return Err(OAuthError::new_static(ErrorCode::InvalidClient, "Invalid client ID"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let requested_scopes = query
|
||||||
|
.scope
|
||||||
|
.to_scopes()
|
||||||
|
.map_err(|err| OAuthError::new(ErrorCode::InvalidGrant, err))?;
|
||||||
|
|
||||||
|
let device_code = Self::generate_token();
|
||||||
|
let user_code = Self::generate_user_code();
|
||||||
|
|
||||||
|
let verification_uri = self
|
||||||
|
.services
|
||||||
|
.config
|
||||||
|
.get_client_domain()
|
||||||
|
.join(&format!("{}/oauth2/grant/device_code", conduwuit::ROUTE_PREFIX))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut verification_uri_complete = verification_uri.clone();
|
||||||
|
verification_uri_complete
|
||||||
|
.query_pairs_mut()
|
||||||
|
.append_pair("user_code", &user_code);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
client_id = &query.client_id,
|
||||||
|
client_name = &client_metadata.client_name,
|
||||||
|
?requested_scopes,
|
||||||
|
"Issuing OAuth device code"
|
||||||
|
);
|
||||||
|
|
||||||
|
let pending_grant = PendingDeviceCodeGrant {
|
||||||
|
state: DeviceCodeGrantState::Unverified { user_code: user_code.clone() },
|
||||||
|
requested_scopes,
|
||||||
|
client_name: client_metadata.client_name,
|
||||||
|
client_id: query.client_id,
|
||||||
|
requested_at: SystemTime::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pending_device_code_grants
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(device_code.clone(), pending_grant);
|
||||||
|
|
||||||
|
Ok(DeviceCodeResponse {
|
||||||
|
device_code,
|
||||||
|
user_code,
|
||||||
|
verification_uri,
|
||||||
|
verification_uri_complete: Some(verification_uri_complete),
|
||||||
|
expires_in: PendingDeviceCodeGrant::MAX_AGE.as_secs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn grant_info_for_user_code(
|
||||||
|
&self,
|
||||||
|
supplied_user_code: &str,
|
||||||
|
) -> Option<DeviceCodeGrantInfo> {
|
||||||
|
let pending_grants = self.pending_device_code_grants.lock().await;
|
||||||
|
|
||||||
|
let (device_code, grant) = pending_grants
|
||||||
|
.iter()
|
||||||
|
.find(|(_, grant)| {
|
||||||
|
matches!(&grant.state, DeviceCodeGrantState::Unverified { user_code } if user_code == supplied_user_code)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let client_metadata = self
|
||||||
|
.get_client_metadata(&grant.client_id)
|
||||||
|
.await
|
||||||
|
.expect("client should exist");
|
||||||
|
|
||||||
|
Some(DeviceCodeGrantInfo {
|
||||||
|
device_code: device_code.clone(),
|
||||||
|
client_metadata,
|
||||||
|
requested_scopes: grant.requested_scopes.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_device_code(
|
||||||
|
&self,
|
||||||
|
authorizing_user: OwnedUserId,
|
||||||
|
device_code: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut pending_grants = self.pending_device_code_grants.lock().await;
|
||||||
|
|
||||||
|
let Some(pending_grant) = pending_grants.get_mut(device_code) else {
|
||||||
|
return Err("Invalid device code".to_owned());
|
||||||
|
};
|
||||||
|
|
||||||
|
match &mut pending_grant.state {
|
||||||
|
| state @ DeviceCodeGrantState::Unverified { .. } => {
|
||||||
|
*state = DeviceCodeGrantState::Verified { authorizing_user };
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
| DeviceCodeGrantState::Verified {
|
||||||
|
authorizing_user: previous_authorizing_user,
|
||||||
|
} =>
|
||||||
|
if *previous_authorizing_user == authorizing_user {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Device code is already verified".to_owned())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse, OAuthError> {
|
pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse, OAuthError> {
|
||||||
|
let TokenRequest { client_id, request } = request;
|
||||||
|
|
||||||
|
let Some(client_metadata) = self.get_client_metadata(&client_id).await else {
|
||||||
|
return Err(OAuthError::new_static(ErrorCode::InvalidClient, "Invalid client ID"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if !client_metadata.grant_types.contains(&request.grant_type()) {
|
||||||
|
return Err(OAuthError::invalid_grant("Client cannot request this grant type"));
|
||||||
|
}
|
||||||
|
|
||||||
match request {
|
match request {
|
||||||
| TokenRequest::AuthorizationCode {
|
| TokenRequestType::AuthorizationCode { code, redirect_uri, code_verifier } => {
|
||||||
code,
|
let mut pending_grants = self.pending_auth_code_grants.lock().await;
|
||||||
redirect_uri,
|
|
||||||
client_id,
|
|
||||||
code_verifier,
|
|
||||||
} => {
|
|
||||||
let mut pending_grants = self.pending_code_grants.lock().await;
|
|
||||||
|
|
||||||
let Some(pending_grant) = pending_grants
|
let Some(pending_grant) = pending_grants
|
||||||
.remove(&code)
|
.remove(&code)
|
||||||
@@ -305,7 +470,39 @@ pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse,
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
},
|
},
|
||||||
| TokenRequest::RefreshToken { client_id, refresh_token } =>
|
| TokenRequestType::DeviceCode { device_code } => {
|
||||||
|
let mut pending_grants = self.pending_device_code_grants.lock().await;
|
||||||
|
|
||||||
|
let Some(pending_grant) = pending_grants
|
||||||
|
.remove(&device_code)
|
||||||
|
.filter(|grant| grant.is_valid_for(&client_id))
|
||||||
|
else {
|
||||||
|
return Err(OAuthError::new_static(
|
||||||
|
ErrorCode::ExpiredToken,
|
||||||
|
"Invalid device code",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
match &pending_grant.state {
|
||||||
|
| DeviceCodeGrantState::Unverified { .. } => {
|
||||||
|
pending_grants.insert(device_code, pending_grant);
|
||||||
|
|
||||||
|
Err(OAuthError::new_static(
|
||||||
|
ErrorCode::AuthorizationPending,
|
||||||
|
"Authorization is pending",
|
||||||
|
))
|
||||||
|
},
|
||||||
|
| DeviceCodeGrantState::Verified { authorizing_user } =>
|
||||||
|
self.create_session(
|
||||||
|
authorizing_user.to_owned(),
|
||||||
|
pending_grant.requested_scopes,
|
||||||
|
pending_grant.client_name,
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
| TokenRequestType::RefreshToken { refresh_token } =>
|
||||||
self.refresh_session(client_id, refresh_token).await,
|
self.refresh_session(client_id, refresh_token).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,11 +561,10 @@ async fn create_session(
|
|||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
return Err(OAuthError {
|
return Err(OAuthError::new_static(
|
||||||
error: ErrorCode::InvalidScope,
|
ErrorCode::InvalidScope,
|
||||||
error_description: "A device with the supplied ID already exists for this user"
|
"A device with the supplied ID already exists for this user",
|
||||||
.into(),
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.services
|
self.services
|
||||||
|
|||||||
@@ -65,17 +65,17 @@ pub(super) async fn for_local_user(services: &Services, user_id: &UserId) -> Sel
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn for_device(
|
pub(super) fn for_device(
|
||||||
oauth_metadata: Option<&ClientMetadata>,
|
client_metadata: Option<&ClientMetadata>,
|
||||||
display_name: Option<&str>,
|
display_name: Option<&str>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let avatar_src = oauth_metadata
|
let avatar_src = client_metadata
|
||||||
.and_then(|metadata| metadata.logo_uri.as_ref())
|
.and_then(|metadata| metadata.logo_uri.as_ref())
|
||||||
.map(|uri| uri.as_str().to_owned());
|
.map(|uri| uri.as_str().to_owned());
|
||||||
|
|
||||||
let avatar_type = if let Some(avatar_src) = avatar_src {
|
let avatar_type = if let Some(avatar_src) = avatar_src {
|
||||||
AvatarType::Image(avatar_src)
|
AvatarType::Image(avatar_src)
|
||||||
} else if let Some(initial) = display_name.and_then(|name| name.chars().next()) {
|
} else if let Some(initial) = display_name.and_then(|name| name.chars().next()) {
|
||||||
if oauth_metadata.is_some() {
|
if client_metadata.is_some() {
|
||||||
AvatarType::Initial(initial)
|
AvatarType::Initial(initial)
|
||||||
} else {
|
} else {
|
||||||
AvatarType::Initial('❖')
|
AvatarType::Initial('❖')
|
||||||
@@ -86,6 +86,20 @@ pub(super) fn for_device(
|
|||||||
|
|
||||||
Self { avatar_type }
|
Self { avatar_type }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn for_client(client_metadata: &ClientMetadata) -> Self {
|
||||||
|
let avatar_type = if let Some(logo) = &client_metadata.logo_uri {
|
||||||
|
AvatarType::Image(logo.to_string())
|
||||||
|
} else if let Some(name) = &client_metadata.client_name
|
||||||
|
&& let Some(char) = name.chars().next()
|
||||||
|
{
|
||||||
|
AvatarType::Initial(char)
|
||||||
|
} else {
|
||||||
|
AvatarType::Initial('?')
|
||||||
|
};
|
||||||
|
|
||||||
|
Self { avatar_type }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
#[derive(Debug, Template)]
|
||||||
|
|||||||
+108
-36
@@ -4,9 +4,12 @@
|
|||||||
response::Redirect,
|
response::Redirect,
|
||||||
routing::on,
|
routing::on,
|
||||||
};
|
};
|
||||||
use conduwuit_service::oauth::grant::{AuthorizationCodeQuery, Prompt};
|
use conduwuit_service::oauth::{
|
||||||
|
client_metadata::ClientMetadata,
|
||||||
|
grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt},
|
||||||
|
};
|
||||||
use ruma::OwnedUserId;
|
use ruma::OwnedUserId;
|
||||||
use url::Url;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ROUTE_PREFIX, WebError,
|
ROUTE_PREFIX, WebError,
|
||||||
@@ -14,7 +17,7 @@
|
|||||||
pages::{
|
pages::{
|
||||||
GET_POST, Result, TemplateContext,
|
GET_POST, Result, TemplateContext,
|
||||||
account::register::{RegisterQuery, RequestedRegistrationFlow},
|
account::register::{RegisterQuery, RequestedRegistrationFlow},
|
||||||
components::{Avatar, AvatarType, ClientScopes},
|
components::{Avatar, ClientScopes, UserCard},
|
||||||
},
|
},
|
||||||
response,
|
response,
|
||||||
session::{LoginIntent, LoginQuery, LoginTarget, User},
|
session::{LoginIntent, LoginQuery, LoginTarget, User},
|
||||||
@@ -22,7 +25,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn build() -> Router<crate::State> {
|
pub(crate) fn build() -> Router<crate::State> {
|
||||||
Router::new().route("/authorization_code", on(GET_POST, route_authorization_code))
|
Router::new()
|
||||||
|
.route("/authorization_code", on(GET_POST, route_authorization_code))
|
||||||
|
.route("/device_code", on(GET_POST, route_device_code))
|
||||||
}
|
}
|
||||||
|
|
||||||
template! {
|
template! {
|
||||||
@@ -30,12 +35,9 @@ struct Grant use "grant.html.j2" {
|
|||||||
logout_query: String,
|
logout_query: String,
|
||||||
user_id: OwnedUserId,
|
user_id: OwnedUserId,
|
||||||
user_avatar: Avatar,
|
user_avatar: Avatar,
|
||||||
client_uri: Url,
|
client_metadata: ClientMetadata,
|
||||||
client_name: String,
|
scopes: ClientScopes,
|
||||||
client_avatar: Avatar,
|
device_code: Option<String>
|
||||||
policy_uri: Option<Url>,
|
|
||||||
tos_uri: Option<Url>,
|
|
||||||
scopes: ClientScopes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,27 +104,6 @@ async fn route_authorization_code(
|
|||||||
|
|
||||||
let scopes = query.scope.to_scopes().map_err(WebError::BadRequest)?;
|
let scopes = query.scope.to_scopes().map_err(WebError::BadRequest)?;
|
||||||
|
|
||||||
let client_name = if let Some(name) = &client.client_name {
|
|
||||||
name
|
|
||||||
} else {
|
|
||||||
"Unknown application"
|
|
||||||
}
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let client_avatar = {
|
|
||||||
let avatar_type = if let Some(logo) = &client.logo_uri {
|
|
||||||
AvatarType::Image(logo.to_string())
|
|
||||||
} else if let Some(name) = &client.client_name
|
|
||||||
&& let Some(char) = name.chars().next()
|
|
||||||
{
|
|
||||||
AvatarType::Initial(char)
|
|
||||||
} else {
|
|
||||||
AvatarType::Initial('?')
|
|
||||||
};
|
|
||||||
|
|
||||||
Avatar { avatar_type }
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
|
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
|
||||||
|
|
||||||
response!(Grant::new(
|
response!(Grant::new(
|
||||||
@@ -135,11 +116,102 @@ async fn route_authorization_code(
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
user_id,
|
user_id,
|
||||||
user_avatar,
|
user_avatar,
|
||||||
client.client_uri.clone(),
|
client,
|
||||||
client_name,
|
|
||||||
client_avatar,
|
|
||||||
client.policy_uri.clone(),
|
|
||||||
client.tos_uri.clone(),
|
|
||||||
ClientScopes { scopes },
|
ClientScopes { scopes },
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(tag = "stage", rename_all = "snake_case")]
|
||||||
|
enum DeviceCodeForm {
|
||||||
|
Lookup {
|
||||||
|
user_code: String,
|
||||||
|
},
|
||||||
|
Confirm {
|
||||||
|
device_code: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
template! {
|
||||||
|
struct DeviceCodeGrant use "device_code_grant.html.j2" {
|
||||||
|
user_card: UserCard,
|
||||||
|
body: DeviceCodeGrantBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DeviceCodeGrantBody {
|
||||||
|
Lookup {
|
||||||
|
user_code_error: bool,
|
||||||
|
},
|
||||||
|
Success,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn route_device_code(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
Extension(context): Extension<TemplateContext>,
|
||||||
|
user: User<true>,
|
||||||
|
Expect(Query(query)): Expect<Query<DeviceCodeVerifyQuery>>,
|
||||||
|
PostForm(form): PostForm<DeviceCodeForm>,
|
||||||
|
) -> Result {
|
||||||
|
let user_id = if let Some(user) = user.into_session() {
|
||||||
|
user.user_id
|
||||||
|
} else {
|
||||||
|
let next = LoginTarget::DeviceCode(query.clone());
|
||||||
|
|
||||||
|
let uri = format!(
|
||||||
|
"{}/account/login?{}",
|
||||||
|
ROUTE_PREFIX,
|
||||||
|
serde_urlencoded::to_string(LoginQuery { next: Some(next), ..Default::default() })
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
return response!(Redirect::to(&uri));
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
|
||||||
|
|
||||||
|
match (form, query.user_code.clone()) {
|
||||||
|
| (None, Some(user_code)) | (Some(DeviceCodeForm::Lookup { user_code }), _) => {
|
||||||
|
let Some(grant_info) = services.oauth.grant_info_for_user_code(&user_code).await
|
||||||
|
else {
|
||||||
|
return response!(DeviceCodeGrant::new(
|
||||||
|
context,
|
||||||
|
user_card,
|
||||||
|
DeviceCodeGrantBody::Lookup { user_code_error: true }
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
|
||||||
|
|
||||||
|
response!(Grant::new(
|
||||||
|
context,
|
||||||
|
serde_urlencoded::to_string(LoginQuery {
|
||||||
|
next: Some(LoginTarget::DeviceCode(query)),
|
||||||
|
intent: Some(LoginIntent::SwitchAccounts),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
user_id,
|
||||||
|
user_avatar,
|
||||||
|
grant_info.client_metadata,
|
||||||
|
ClientScopes { scopes: grant_info.requested_scopes },
|
||||||
|
Some(grant_info.device_code),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
| (Some(DeviceCodeForm::Confirm { device_code }), _) => {
|
||||||
|
services
|
||||||
|
.oauth
|
||||||
|
.validate_device_code(user_id, &device_code)
|
||||||
|
.await
|
||||||
|
.map_err(WebError::BadRequest)?;
|
||||||
|
|
||||||
|
response!(DeviceCodeGrant::new(context, user_card, DeviceCodeGrantBody::Success))
|
||||||
|
},
|
||||||
|
| (None, None) =>
|
||||||
|
response!(DeviceCodeGrant::new(context, user_card, DeviceCodeGrantBody::Lookup {
|
||||||
|
user_code_error: false
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,3 +20,17 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-code {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 200%;
|
||||||
|
padding: 0.2em;
|
||||||
|
width: 6em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{% extends "_layout.html.j2" %}
|
||||||
|
|
||||||
|
{%- block head -%}
|
||||||
|
<link rel="stylesheet" href="{{ crate::ROUTE_PREFIX }}/resources/grant.css">
|
||||||
|
{%- endblock -%}
|
||||||
|
|
||||||
|
{%- block title -%}
|
||||||
|
Authorize client
|
||||||
|
{%- endblock -%}
|
||||||
|
|
||||||
|
{%- block content -%}
|
||||||
|
<div class="panel narrow">
|
||||||
|
<h1>Authorize device</h1>
|
||||||
|
{{ user_card }}
|
||||||
|
{% match body %}
|
||||||
|
{% when DeviceCodeGrantBody::Lookup { user_code_error } %}
|
||||||
|
<p>
|
||||||
|
Enter the code displayed on your device, or in the app you're signing into.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em class="negative">Never enter a code given to you by someone else.</em>
|
||||||
|
Your homeserver administrator will never ask you to enter a code on this page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<p>
|
||||||
|
<div class="user-code">
|
||||||
|
<label for="user_code">Validation code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="user_code"
|
||||||
|
name="user_code"
|
||||||
|
maxlength="6"
|
||||||
|
spellcheck="false"
|
||||||
|
pattern="\d{6}"
|
||||||
|
title="Enter the six-digit validation code"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{% if user_code_error %}
|
||||||
|
<small class="error">Invalid code</small>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<input type="hidden" name="stage" value="lookup">
|
||||||
|
<button type="submit">Continue</button>
|
||||||
|
</form>
|
||||||
|
{% when DeviceCodeGrantBody::Success %}
|
||||||
|
<p>Success! 🎉 You can close this tab and return to your device.</p>
|
||||||
|
{% endmatch %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -9,6 +9,7 @@ Authorize client
|
|||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
|
{% let client_name = client_metadata.client_name.as_deref().unwrap_or("Unknown application") %}
|
||||||
<div class="panel narrow">
|
<div class="panel narrow">
|
||||||
<h1>Authorize {{ client_name }}</h1>
|
<h1>Authorize {{ client_name }}</h1>
|
||||||
<div class="avatars">
|
<div class="avatars">
|
||||||
@@ -18,17 +19,17 @@ Authorize client
|
|||||||
<div class="separator" aria-hidden="true">
|
<div class="separator" aria-hidden="true">
|
||||||
⇄
|
⇄
|
||||||
</div>
|
</div>
|
||||||
{{ client_avatar }}
|
{{ Avatar::for_client(client_metadata) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="identity">
|
<div class="identity">
|
||||||
Signed in as <code>{{ user_id }}</code>. <a href="{{ crate::ROUTE_PREFIX }}/account/logout?{{ logout_query }}">Switch accounts</a>
|
Signed in as <code>{{ user_id }}</code>. <a href="{{ crate::ROUTE_PREFIX }}/account/logout?{{ logout_query }}">Switch accounts</a>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<b>{{ client_name }}</b> (<a href="{{ client_uri }}">{{ client_uri.domain().unwrap() }}</a>) would like
|
<b>{{ client_name }}</b> (<a href="{{ client_metadata.client_uri }}">{{ client_metadata.client_uri.domain().unwrap() }}</a>) would like
|
||||||
your permission to:
|
your permission to:
|
||||||
{{ scopes }}
|
{{ scopes }}
|
||||||
</p>
|
</p>
|
||||||
{% match (&policy_uri, &tos_uri) %}
|
{% match (&client_metadata.policy_uri, &client_metadata.tos_uri) %}
|
||||||
{% when (Some(policy_uri), Some(tos_uri)) %}
|
{% when (Some(policy_uri), Some(tos_uri)) %}
|
||||||
<p>
|
<p>
|
||||||
{{ client_name }}'s <a href="{{ policy_uri }}">policies</a>
|
{{ client_name }}'s <a href="{{ policy_uri }}">policies</a>
|
||||||
@@ -49,6 +50,10 @@ Authorize client
|
|||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden" name="stage" value="confirm">
|
||||||
|
{% if let Some(device_code) = device_code %}
|
||||||
|
<input type="hidden" name="device_code" value="{{ device_code }}">
|
||||||
|
{% endif %}
|
||||||
<button type="submit">Continue</button>
|
<button type="submit">Continue</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
http::request::Parts,
|
http::request::Parts,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
};
|
};
|
||||||
use conduwuit_service::oauth::grant::AuthorizationCodeQuery;
|
use conduwuit_service::oauth::grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery};
|
||||||
use ruma::{OwnedUserId, UserId};
|
use ruma::{OwnedUserId, UserId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
@@ -38,6 +38,7 @@ pub(crate) enum LoginTarget {
|
|||||||
ChangeEmail,
|
ChangeEmail,
|
||||||
CrossSigningReset,
|
CrossSigningReset,
|
||||||
Deactivate,
|
Deactivate,
|
||||||
|
DeviceCode(DeviceCodeVerifyQuery),
|
||||||
DeviceInfo(DevicePath),
|
DeviceInfo(DevicePath),
|
||||||
RemoveDevice(DevicePath),
|
RemoveDevice(DevicePath),
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,9 @@ pub(crate) fn target_path(&self) -> String {
|
|||||||
| Self::ChangeEmail => "account/email/change/".into(),
|
| Self::ChangeEmail => "account/email/change/".into(),
|
||||||
| Self::CrossSigningReset => "account/cross_signing_reset".into(),
|
| Self::CrossSigningReset => "account/cross_signing_reset".into(),
|
||||||
| Self::Deactivate => "account/deactivate".into(),
|
| Self::Deactivate => "account/deactivate".into(),
|
||||||
|
| Self::DeviceCode(code) =>
|
||||||
|
format!("oauth2/grant/device_code?{}", serde_urlencoded::to_string(code).unwrap())
|
||||||
|
.into(),
|
||||||
| Self::DeviceInfo(path) => format!("account/device/{}/", path.device).into(),
|
| Self::DeviceInfo(path) => format!("account/device/{}/", path.device).into(),
|
||||||
| Self::RemoveDevice(path) => format!("account/device/{}/remove", path.device).into(),
|
| Self::RemoveDevice(path) => format!("account/device/{}/remove", path.device).into(),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user