Lines
76.11 %
Functions
100 %
Branches
use std::{borrow::Cow, sync::Arc};
use async_trait::async_trait;
use chrono::{TimeDelta, Utc};
use log::error;
use oxide_auth::primitives::{
grant::{Extensions, Grant},
issuer::{IssuedToken, RefreshedToken, TokenType},
registrar::{BoundClient, ClientUrl, ExactUrl, PreGrant, RegisteredUrl, RegistrarError},
scope::Scope,
};
use oxide_auth_async::primitives::{Authorizer, Issuer, Registrar};
use sylvia_iot_corelib::{err::E_UNKNOWN, strings};
use crate::models::{
access_token::{self, AccessToken, QueryCond as AccessTokenQuery},
authorization_code::{self, AuthorizationCode, QueryCond as AuthorizationCodeQuery},
client::QueryCond,
refresh_token::{self, QueryCond as RefreshTokenQuery, RefreshToken},
Model,
#[derive(Clone)]
pub struct Primitive {
model: Arc<dyn Model>,
}
impl Primitive {
pub fn new(model: Arc<dyn Model>) -> Self {
Primitive {
model: model.clone(),
#[async_trait]
impl Authorizer for Primitive {
async fn authorize(&mut self, grant: Grant) -> Result<String, ()> {
const FN_NAME: &'static str = "authorize";
let scope = grant.scope.to_string();
let code = AuthorizationCode {
code: strings::random_id_sha(&grant.until, 4),
expires_at: match TimeDelta::try_seconds(authorization_code::EXPIRES) {
None => panic!("{}", E_UNKNOWN),
Some(t) => Utc::now() + t,
},
redirect_uri: grant.redirect_uri.to_string(),
scope: match scope.len() {
0 => None,
_ => Some(scope),
client_id: grant.client_id,
user_id: grant.owner_id,
match self.model.authorization_code().add(&code).await {
Err(e) => {
error!("[{}] add authorization code error: {}", FN_NAME, e);
Err(())
Ok(()) => Ok(code.code),
async fn extract(&mut self, code: &str) -> Result<Option<Grant>, ()> {
const FN_NAME: &'static str = "extract";
let auth_code = match self.model.authorization_code().get(code).await {
Err(_) => return Err(()),
Ok(code) => match code {
None => return Ok(None),
Some(code) => code,
{
let query = AuthorizationCodeQuery {
code: Some(code),
..Default::default()
if let Err(e) = self.model.authorization_code().del(&query).await {
error!("[{}] delete authorization code error: {}", FN_NAME, e);
return Err(());
if auth_code.expires_at < Utc::now() {
return Ok(None);
Ok(Some(Grant {
owner_id: auth_code.user_id,
client_id: auth_code.client_id,
scope: match auth_code.scope {
None => "".parse().unwrap(),
Some(scope) => match scope.as_str().parse() {
error!("[{}] parse authorization code scope error: {}", FN_NAME, e);
Ok(scope) => scope,
redirect_uri: match auth_code.redirect_uri.parse() {
error!(
"[{}] parse authorization code redirect_uri error: {}",
FN_NAME, e
);
Ok(uri) => uri,
until: auth_code.expires_at,
extensions: Extensions::new(),
}))
impl Issuer for Primitive {
async fn issue(&mut self, grant: Grant) -> Result<IssuedToken, ()> {
const FN_NAME: &'static str = "issue";
let now = Utc::now();
let refresh_token = strings::random_id_sha(&now, 8);
let access = AccessToken {
access_token: strings::random_id_sha(&now, 8),
refresh_token: Some(refresh_token.clone()),
expires_at: match TimeDelta::try_seconds(access_token::EXPIRES) {
Some(t) => now + t,
scope: Some(grant.scope.to_string()),
client_id: grant.client_id.clone(),
user_id: grant.owner_id.clone(),
let refresh = RefreshToken {
refresh_token,
expires_at: match TimeDelta::try_seconds(refresh_token::EXPIRES) {
if let Err(e) = self.model.access_token().add(&access).await {
error!("[{}] add access token error: {}", FN_NAME, e);
if let Err(e) = self.model.refresh_token().add(&refresh).await {
error!("[{}] add refresh token error: {}", FN_NAME, e);
Ok(IssuedToken {
token: access.access_token,
refresh: Some(refresh.refresh_token),
until: access.expires_at,
token_type: TokenType::Bearer,
})
async fn refresh(&mut self, token: &str, grant: Grant) -> Result<RefreshedToken, ()> {
const FN_NAME: &'static str = "refresh";
let query = AccessTokenQuery {
refresh_token: Some(token),
if let Err(e) = self.model.access_token().del(&query).await {
error!("[{}] delete access token error: {}", FN_NAME, e);
let query = RefreshTokenQuery {
if let Err(e) = self.model.refresh_token().del(&query).await {
error!("[{}] delete refresh token error: {}", FN_NAME, e);
match self.issue(grant).await {
Err(_) => Err(()),
Ok(token) => Ok(RefreshedToken {
token: token.token,
refresh: token.refresh,
until: token.until,
token_type: token.token_type,
}),
async fn recover_token(&mut self, token: &str) -> Result<Option<Grant>, ()> {
const FN_NAME: &'static str = "recover_token";
let access = match self.model.access_token().get(token).await {
error!("[{}] get access token error: {}", FN_NAME, e);
Ok(token) => match token {
Some(token) => token,
if access.expires_at < Utc::now() {
owner_id: access.user_id,
client_id: access.client_id,
scope: match access.scope {
error!("[{}] parse access token scope error: {}", FN_NAME, e);
redirect_uri: match access.redirect_uri.parse() {
error!("[{}] parse access token redirect_uri error: {}", FN_NAME, e);
async fn recover_refresh(&mut self, token: &str) -> Result<Option<Grant>, ()> {
const FN_NAME: &'static str = "recover_refresh";
let refresh = match self.model.refresh_token().get(token).await {
error!("[{}] get refresh token error: {}", FN_NAME, e);
if refresh.expires_at < Utc::now() {
owner_id: refresh.user_id,
client_id: refresh.client_id,
scope: match refresh.scope {
redirect_uri: match refresh.redirect_uri.parse() {
until: refresh.expires_at,
impl Registrar for Primitive {
async fn bound_redirect<'a>(
&self,
bound: ClientUrl<'a>,
) -> Result<BoundClient<'a>, RegistrarError> {
const FN_NAME: &'static str = "bound_redirect";
let cond = QueryCond {
client_id: Some(&bound.client_id),
let redirect_uris = match self.model.client().get(&cond).await {
error!("[{}] get client error: {}", FN_NAME, e);
return Err(RegistrarError::PrimitiveError);
Ok(client) => match client {
None => return Err(RegistrarError::Unspecified),
Some(client) => client.redirect_uris,
let redirect_uri = match bound.redirect_uri {
None => match redirect_uris.len() {
0 => return Err(RegistrarError::Unspecified),
_ => redirect_uris.get(0).unwrap(),
Some(url) => match redirect_uris
.iter()
.find(|uri| uri.as_str() == url.as_str())
Some(uri) => uri,
let redirect_uri = match ExactUrl::new(redirect_uri.clone()) {
Err(_) => return Err(RegistrarError::Unspecified),
Ok(url) => url,
Ok(BoundClient {
client_id: bound.client_id,
redirect_uri: Cow::Owned(RegisteredUrl::Exact(redirect_uri)),
async fn negotiate<'a>(
bound: BoundClient<'a>,
scope: Option<Scope>,
) -> Result<PreGrant, RegistrarError> {
const FN_NAME: &'static str = "negotiate";
let client = match self.model.client().get(&cond).await {
return {
Err(RegistrarError::PrimitiveError)
Some(client) => client,
if client.scopes.len() > 0 {
match scope {
Some(scope) => {
let client_scopes = match client.scopes.join(" ").parse::<Scope>() {
error!("[{}] parse client scope error: {}", FN_NAME, e);
Ok(scopes) => scopes,
if !scope.allow_access(&client_scopes) {
return Err(RegistrarError::Unspecified);
Ok(PreGrant {
client_id: bound.client_id.into_owned(),
redirect_uri: bound.redirect_uri.into_owned(),
scope: match client.scopes.join(" ").parse() {
async fn check(
client_id: &str,
passphrase: Option<&[u8]>,
) -> Result<(), RegistrarError> {
const FN_NAME: &'static str = "check";
client_id: Some(client_id),
match (passphrase, client.client_secret) {
(None, None) => Ok(()),
(Some(passphrase), Some(client_secret)) => {
match passphrase == client_secret.as_bytes() {
true => Ok(()),
false => Err(RegistrarError::Unspecified),
_ => Err(RegistrarError::Unspecified),