Can request refresh tokens
This commit is contained in:
		| @@ -45,10 +45,18 @@ pub struct PushNewSession(pub Session); | |||||||
| #[rtype(result = "Option<Session>")] | #[rtype(result = "Option<Session>")] | ||||||
| pub struct FindSessionByAuthorizationCode(pub String); | pub struct FindSessionByAuthorizationCode(pub String); | ||||||
|  |  | ||||||
|  | #[derive(Message)] | ||||||
|  | #[rtype(result = "Option<Session>")] | ||||||
|  | pub struct FindSessionByRefreshToken(pub String); | ||||||
|  |  | ||||||
| #[derive(Message)] | #[derive(Message)] | ||||||
| #[rtype(result = "()")] | #[rtype(result = "()")] | ||||||
| pub struct MarkAuthorizationCodeUsed(pub String); | pub struct MarkAuthorizationCodeUsed(pub String); | ||||||
|  |  | ||||||
|  | #[derive(Message)] | ||||||
|  | #[rtype(result = "()")] | ||||||
|  | pub struct UpdateSession(pub Session); | ||||||
|  |  | ||||||
| #[derive(Default)] | #[derive(Default)] | ||||||
| pub struct OpenIDSessionsActor { | pub struct OpenIDSessionsActor { | ||||||
|     session: Vec<Session>, |     session: Vec<Session>, | ||||||
| @@ -91,6 +99,17 @@ impl Handler<FindSessionByAuthorizationCode> for OpenIDSessionsActor { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl Handler<FindSessionByRefreshToken> for OpenIDSessionsActor { | ||||||
|  |     type Result = Option<Session>; | ||||||
|  |  | ||||||
|  |     fn handle(&mut self, msg: FindSessionByRefreshToken, _ctx: &mut Self::Context) -> Self::Result { | ||||||
|  |         self.session | ||||||
|  |             .iter() | ||||||
|  |             .find(|f| f.refresh_token.eq(&msg.0)) | ||||||
|  |             .cloned() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor { | impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor { | ||||||
|     type Result = (); |     type Result = (); | ||||||
|  |  | ||||||
| @@ -102,3 +121,14 @@ impl Handler<MarkAuthorizationCodeUsed> for OpenIDSessionsActor { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl Handler<UpdateSession> for OpenIDSessionsActor { | ||||||
|  |     type Result = (); | ||||||
|  |  | ||||||
|  |     fn handle(&mut self, msg: UpdateSession, _ctx: &mut Self::Context) -> Self::Result { | ||||||
|  |         if let Some(r) = self.session.iter().enumerate() | ||||||
|  |             .find(|f| f.1.session_id.eq(&msg.0.session_id)).map(|f| f.0) { | ||||||
|  |             self.session[r] = msg.0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -152,13 +152,29 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author | |||||||
|         ))).finish() |         ))).finish() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, serde::Deserialize)] | ||||||
|  | pub struct TokenAuthorizationCodeQuery { | ||||||
|  |     redirect_uri: String, | ||||||
|  |     code: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, serde::Deserialize)] | ||||||
|  | pub struct TokenRefreshTokenQuery { | ||||||
|  |     refresh_token: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Debug, serde::Deserialize)] | #[derive(Debug, serde::Deserialize)] | ||||||
| pub struct TokenQuery { | pub struct TokenQuery { | ||||||
|     grant_type: String, |     grant_type: String, | ||||||
|     client_id: Option<ClientID>, |     client_id: Option<ClientID>, | ||||||
|     client_secret: Option<String>, |     client_secret: Option<String>, | ||||||
|     redirect_uri: String, |  | ||||||
|     code: String, |     #[serde(flatten)] | ||||||
|  |     authorization_code_query: Option<TokenAuthorizationCodeQuery>, | ||||||
|  |  | ||||||
|  |     #[serde(flatten)] | ||||||
|  |     refresh_token_query: Option<TokenRefreshTokenQuery>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, serde::Serialize)] | #[derive(Debug, serde::Serialize)] | ||||||
| @@ -167,7 +183,8 @@ pub struct TokenResponse { | |||||||
|     token_type: &'static str, |     token_type: &'static str, | ||||||
|     refresh_token: String, |     refresh_token: String, | ||||||
|     expires_in: u64, |     expires_in: u64, | ||||||
|     id_token: String, |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |     id_token: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn token(req: HttpRequest, | pub async fn token(req: HttpRequest, | ||||||
| @@ -176,8 +193,7 @@ pub async fn token(req: HttpRequest, | |||||||
|                    app_config: web::Data<AppConfig>, |                    app_config: web::Data<AppConfig>, | ||||||
|                    sessions: web::Data<Addr<OpenIDSessionsActor>>, |                    sessions: web::Data<Addr<OpenIDSessionsActor>>, | ||||||
|                    jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> { |                    jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> { | ||||||
|     // TODO : add refresh tokens : https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens |     // TODO : check auth challenge : https://oa.dnc.global/-fr-.html?page=unarticle&id_article=148&lang=fr | ||||||
|     // TODO : check auth challenge |  | ||||||
|  |  | ||||||
|     // Extraction authentication information |     // Extraction authentication information | ||||||
|     let authorization_header = req.headers().get("authorization"); |     let authorization_header = req.headers().get("authorization"); | ||||||
| @@ -226,68 +242,111 @@ pub async fn token(req: HttpRequest, | |||||||
|         return Ok(HttpResponse::Unauthorized().body("Client secret is invalid!")); |         return Ok(HttpResponse::Unauthorized().body("Client secret is invalid!")); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if query.grant_type != "authorization_code" { |     let token_response = match (query.grant_type.as_str(), | ||||||
|         log::warn!("Token request failed: Grant type unsupported! {:#?}", query.0); |                                 &query.authorization_code_query, | ||||||
|         return Ok(HttpResponse::BadRequest().body("Grant type unsupported!")); |                                 &query.refresh_token_query) { | ||||||
|     } |         ("authorization_code", Some(q), _) => { | ||||||
|  |             let session: Session = match sessions | ||||||
|  |                 .send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone())) | ||||||
|  |                 .await.unwrap() | ||||||
|  |             { | ||||||
|  |                 None => { | ||||||
|  |                     log::warn!("Token request failed: Session not found! {:#?}", query.0); | ||||||
|  |                     return Ok(HttpResponse::NotFound().body("Session not found!")); | ||||||
|  |                 } | ||||||
|  |                 Some(s) => s, | ||||||
|  |             }; | ||||||
|  |  | ||||||
|     let session: Session = match sessions |             if session.client != client.id { | ||||||
|         .send(openid_sessions_actor::FindSessionByAuthorizationCode(query.code.clone())) |                 log::warn!("Token request failed: Client mismatch! {:#?}", query.0); | ||||||
|         .await.unwrap() |                 return Ok(HttpResponse::Unauthorized().body("Client mismatch!")); | ||||||
|     { |             } | ||||||
|         None => { |  | ||||||
|             log::warn!("Token request failed: Session not found! {:#?}", query.0); |             if session.redirect_uri != q.redirect_uri { | ||||||
|             return Ok(HttpResponse::NotFound().body("Session not found!")); |                 log::warn!("Token request failed: Invalid redirect URI! {:#?}", query.0); | ||||||
|  |                 return Ok(HttpResponse::Unauthorized().body("Invalid redirect URI!")); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if session.authorization_code_expire_at < time() { | ||||||
|  |                 log::warn!("Token request failed: Authorization code expired! {:#?}", query.0); | ||||||
|  |                 return Ok(HttpResponse::Unauthorized().body("Authorization code expired!")); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if session.authorization_code_used { | ||||||
|  |                 log::warn!("Token request failed: Authorization already used! {:#?}", query.0); | ||||||
|  |                 return Ok(HttpResponse::Unauthorized().body("Authorization already used!")); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Mark session as used | ||||||
|  |             sessions.send(openid_sessions_actor::MarkAuthorizationCodeUsed(session.authorization_code)) | ||||||
|  |                 .await.unwrap(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |             // Generate id token | ||||||
|  |             let token = IdToken { | ||||||
|  |                 issuer: app_config.website_origin.to_string(), | ||||||
|  |                 subject_identifier: session.user, | ||||||
|  |                 audience: session.client.0.to_string(), | ||||||
|  |                 expiration_time: session.access_token_expire_at, | ||||||
|  |                 issued_at: time(), | ||||||
|  |                 auth_time: session.auth_time, | ||||||
|  |                 nonce: session.nonce, | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             TokenResponse { | ||||||
|  |                 access_token: session.access_token, | ||||||
|  |                 token_type: "Bearer", | ||||||
|  |                 refresh_token: session.refresh_token, | ||||||
|  |                 expires_in: session.access_token_expire_at - time(), | ||||||
|  |                 id_token: Some(jwt_signer.sign_token(token.to_jwt_claims())?), | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         Some(s) => s, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     if session.client != client.id { |         ("refresh_token", _, Some(q)) => { | ||||||
|         log::warn!("Token request failed: Client mismatch! {:#?}", query.0); |             let mut session: Session = match sessions | ||||||
|         return Ok(HttpResponse::Unauthorized().body("Client mismatch!")); |                 .send(openid_sessions_actor::FindSessionByRefreshToken(q.refresh_token.clone())) | ||||||
|     } |                 .await.unwrap() | ||||||
|  |             { | ||||||
|  |                 None => { | ||||||
|  |                     log::warn!("Token refresh failed: Session not found! {:#?}", query.0); | ||||||
|  |                     return Ok(HttpResponse::NotFound().body("Session not found!")); | ||||||
|  |                 } | ||||||
|  |                 Some(s) => s, | ||||||
|  |             }; | ||||||
|  |  | ||||||
|     if session.redirect_uri != query.redirect_uri { |             if session.client != client.id { | ||||||
|         log::warn!("Token request failed: Invalid redirect URI! {:#?}", query.0); |                 log::warn!("Token request failed: Client mismatch! {:#?}", query.0); | ||||||
|         return Ok(HttpResponse::Unauthorized().body("Invalid redirect URI!")); |                 return Ok(HttpResponse::Unauthorized().body("Client mismatch!")); | ||||||
|     } |             } | ||||||
|  |  | ||||||
|     if session.authorization_code_expire_at < time() { |             session.refresh_token = rand_str(OPEN_ID_REFRESH_TOKEN_LEN); | ||||||
|         log::warn!("Token request failed: Authorization code expired! {:#?}", query.0); |             session.refresh_token_expire_at = OPEN_ID_REFRESH_TOKEN_TIMEOUT + time(); | ||||||
|         return Ok(HttpResponse::Unauthorized().body("Authorization code expired!")); |             session.access_token = rand_str(OPEN_ID_ACCESS_TOKEN_LEN); | ||||||
|     } |             session.access_token_expire_at = OPEN_ID_ACCESS_TOKEN_TIMEOUT + time(); | ||||||
|  |  | ||||||
|     if session.authorization_code_used { |             sessions | ||||||
|         log::warn!("Token request failed: Authorization already used! {:#?}", query.0); |                 .send(openid_sessions_actor::UpdateSession(session.clone())) | ||||||
|         return Ok(HttpResponse::Unauthorized().body("Authorization already used!")); |                 .await.unwrap(); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Mark session as used |             TokenResponse { | ||||||
|     sessions.send(openid_sessions_actor::MarkAuthorizationCodeUsed(session.authorization_code)) |                 access_token: session.access_token, | ||||||
|         .await.unwrap(); |                 token_type: "Bearer", | ||||||
|  |                 refresh_token: session.refresh_token, | ||||||
|  |                 expires_in: session.access_token_expire_at - time(), | ||||||
|  |                 id_token: None, | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         _ => { | ||||||
|     // Generate id token |             log::warn!("Token request failed: Grant type unsupported or incomplete! {:#?}", query.0); | ||||||
|     let token = IdToken { |             return Ok(HttpResponse::BadRequest().body("Grant type unsupported!")); | ||||||
|         issuer: app_config.website_origin.to_string(), |         } | ||||||
|         subject_identifier: session.user, |  | ||||||
|         audience: session.client.0.to_string(), |  | ||||||
|         expiration_time: session.access_token_expire_at, |  | ||||||
|         issued_at: time(), |  | ||||||
|         auth_time: session.auth_time, |  | ||||||
|         nonce: session.nonce, |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     Ok(HttpResponse::Ok() |     Ok(HttpResponse::Ok() | ||||||
|         .append_header(("Cache-Control", "no-store")) |         .append_header(("Cache-Control", "no-store")) | ||||||
|         .append_header(("Pragam", "no-cache")) |         .append_header(("Pragam", "no-cache")) | ||||||
|         .json(TokenResponse { |         .json(token_response)) | ||||||
|             access_token: session.access_token, |  | ||||||
|             token_type: "Bearer", |  | ||||||
|             refresh_token: session.refresh_token, |  | ||||||
|             expires_in: session.access_token_expire_at - time(), |  | ||||||
|             id_token: jwt_signer.sign_token(token.to_jwt_claims())? |  | ||||||
|         })) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(serde::Serialize)] | #[derive(serde::Serialize)] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user