mirror of
https://github.com/System-End/slack-morphism-rust.git
synced 2026-04-19 22:05:15 +00:00
Improved OAuth/signature verifier data types and error handling (#133)
This commit is contained in:
parent
4be723918d
commit
4923fb7d45
10 changed files with 143 additions and 59 deletions
17
README.md
17
README.md
|
|
@ -33,6 +33,23 @@ Routes for this example are available on http://<your-host>:8080:
|
|||
- /interaction - for Slack Interaction Events
|
||||
- /command - for Slack Command Events
|
||||
|
||||
### Testing with ngrok
|
||||
For development/testing purposes you can use [ngrok](https://ngrok.com/):
|
||||
```
|
||||
ngrok http 8080
|
||||
```
|
||||
and copy the URL it gives for you to the example parameters for `SLACK_REDIRECT_HOST`.
|
||||
|
||||
Example testing with ngrok:
|
||||
```
|
||||
SLACK_CLIENT_ID=<your-client-id> \
|
||||
SLACK_CLIENT_SECRET=<your-client-secret> \
|
||||
SLACK_BOT_SCOPE=app_mentions:read,incoming-webhook \
|
||||
SLACK_REDIRECT_HOST=https://<your-ngrok-url>.ngrok.io \
|
||||
SLACK_SIGNING_SECRET=<your-signing-secret> \
|
||||
cargo run --example events_api_server
|
||||
```
|
||||
|
||||
## Licence
|
||||
Apache Software License (ASL)
|
||||
|
||||
|
|
|
|||
|
|
@ -114,23 +114,23 @@ async fn create_slack_events_listener_server() -> Result<(), Box<dyn std::error:
|
|||
// You can additionally configure HTTP route paths using theses configs,
|
||||
// but for simplicity we will skip that part here and configure only required parameters.
|
||||
let oauth_listener_config = Arc::new(SlackOAuthListenerConfig::new(
|
||||
std::env::var("SLACK_CLIENT_ID")?,
|
||||
std::env::var("SLACK_CLIENT_SECRET")?,
|
||||
std::env::var("SLACK_BOT_SCOPE")?,
|
||||
std::env::var("SLACK_REDIRECT_HOST")?,
|
||||
config_env_var("SLACK_CLIENT_ID")?.into(),
|
||||
config_env_var("SLACK_CLIENT_SECRET")?.into(),
|
||||
config_env_var("SLACK_BOT_SCOPE")?,
|
||||
config_env_var("SLACK_REDIRECT_HOST")?,
|
||||
));
|
||||
|
||||
let push_events_config = Arc::new(SlackPushEventsListenerConfig::new(std::env::var(
|
||||
"SLACK_SIGNING_SECRET",
|
||||
)?));
|
||||
let push_events_config = Arc::new(SlackPushEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let interactions_events_config = Arc::new(SlackInteractionEventsListenerConfig::new(
|
||||
std::env::var("SLACK_SIGNING_SECRET")?,
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let command_events_config = Arc::new(SlackCommandEventsListenerConfig::new(std::env::var(
|
||||
"SLACK_SIGNING_SECRET",
|
||||
)?));
|
||||
let command_events_config = Arc::new(SlackCommandEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
// Creating a shared listener environment with an ability to share client and user state
|
||||
let listener_environment = Arc::new(
|
||||
|
|
@ -189,11 +189,27 @@ async fn create_slack_events_listener_server() -> Result<(), Box<dyn std::error:
|
|||
}
|
||||
```
|
||||
|
||||
Also the library provides Slack events signature verifier (`SlackEventSignatureVerifier`),
|
||||
which is already integrated in the routes implementation for you and you don't need to use
|
||||
it directly. All you need is provide your client id and secret configuration
|
||||
to route implementation.
|
||||
## Testing with ngrok
|
||||
For development/testing purposes you can use [ngrok](https://ngrok.com/):
|
||||
```
|
||||
ngrok http 8080
|
||||
```
|
||||
and copy the URL it gives for you to the example parameters for `SLACK_REDIRECT_HOST`.
|
||||
|
||||
Example testing with ngrok:
|
||||
```
|
||||
SLACK_CLIENT_ID=<your-client-id> \
|
||||
SLACK_CLIENT_SECRET=<your-client-secret> \
|
||||
SLACK_BOT_SCOPE=app_mentions:read,incoming-webhook \
|
||||
SLACK_REDIRECT_HOST=https://<your-ngrok-url>.ngrok.io \
|
||||
SLACK_SIGNING_SECRET=<your-signing-secret> \
|
||||
cargo run --example events_api_server
|
||||
```
|
||||
|
||||
## Slack Signature Verifier
|
||||
The library provides Slack events signature verifier (`SlackEventSignatureVerifier`),
|
||||
which is already integrated in the OAuth routes implementation for you, and you don't need to use it directly.
|
||||
All you need is provide your client id and secret configuration to route implementation.
|
||||
Look at the [complete example here](https://github.com/abdolence/slack-morphism-rust/tree/master/src/hyper/examples/events_api_server.rs).
|
||||
|
||||
|
||||
In case you're embedding the library into your own Web/routes-framework, you can use it separately.
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
//!
|
||||
|
||||
use rsb_derive::Builder;
|
||||
use rvstruct::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::fmt;
|
||||
|
||||
use crate::client::*;
|
||||
use crate::token::*;
|
||||
|
|
@ -25,8 +27,14 @@ where
|
|||
let full_uri: Url = SlackClientHttpApiUri::create_url_with_params(
|
||||
&SlackClientHttpApiUri::create_method_uri_path("oauth.v2.access"),
|
||||
&vec![
|
||||
("code", Some(&req.code)),
|
||||
("redirect_uri", req.redirect_uri.as_ref()),
|
||||
("code", Some(req.code.value())),
|
||||
(
|
||||
"redirect_uri",
|
||||
req.redirect_uri
|
||||
.as_ref()
|
||||
.map(|url| url.as_str().to_string())
|
||||
.as_ref(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -42,14 +50,14 @@ where
|
|||
pub struct SlackOAuthV2AccessTokenRequest {
|
||||
pub client_id: SlackClientId,
|
||||
pub client_secret: SlackClientSecret,
|
||||
pub code: String,
|
||||
pub redirect_uri: Option<String>,
|
||||
pub code: SlackOAuthCode,
|
||||
pub redirect_uri: Option<Url>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackOAuthV2AccessTokenResponse {
|
||||
pub access_token: String,
|
||||
pub access_token: SlackApiTokenValue,
|
||||
pub token_type: SlackApiTokenType,
|
||||
pub scope: SlackApiTokenScope,
|
||||
pub bot_user_id: Option<SlackUserId>,
|
||||
|
|
@ -64,7 +72,7 @@ pub struct SlackOAuthV2AccessTokenResponse {
|
|||
pub struct SlackOAuthV2AuthedUser {
|
||||
pub id: SlackUserId,
|
||||
pub scope: Option<SlackApiTokenScope>,
|
||||
pub access_token: Option<String>,
|
||||
pub access_token: Option<SlackApiTokenValue>,
|
||||
pub token_type: Option<SlackApiTokenType>,
|
||||
}
|
||||
|
||||
|
|
@ -76,3 +84,12 @@ pub struct SlackOAuthIncomingWebHook {
|
|||
pub configuration_url: Url,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct SlackOAuthCode(pub String);
|
||||
|
||||
impl fmt::Debug for SlackOAuthCode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "SlackOAuthCode(len:{})", self.value().len())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::error::Error;
|
|||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::time::Duration;
|
||||
use url::ParseError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SlackClientError {
|
||||
|
|
@ -190,3 +191,11 @@ impl Display for SlackRateLimitError {
|
|||
}
|
||||
|
||||
impl Error for SlackRateLimitError {}
|
||||
|
||||
impl From<url::ParseError> for SlackClientError {
|
||||
fn from(url_parse_error: ParseError) -> Self {
|
||||
SlackClientError::HttpProtocolError(
|
||||
SlackClientHttpProtocolError::new().with_cause(Box::new(url_parse_error)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
use crate::{SlackClient, SlackClientHttpConnector};
|
||||
use crate::{ClientResult, SlackClient, SlackClientHttpConnector};
|
||||
use futures::executor::block_on;
|
||||
use futures::FutureExt;
|
||||
use rsb_derive::Builder;
|
||||
use slack_morphism_models::{SlackClientId, SlackClientSecret, SlackSigningSecret};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
|
||||
type UserStatesMap = HashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>;
|
||||
|
||||
|
|
@ -100,7 +102,7 @@ pub type ErrorHandler<SCHC> = fn(
|
|||
|
||||
#[derive(Debug, PartialEq, Clone, Builder)]
|
||||
pub struct SlackCommandEventsListenerConfig {
|
||||
pub events_signing_secret: String,
|
||||
pub events_signing_secret: SlackSigningSecret,
|
||||
#[default = "SlackCommandEventsListenerConfig::DEFAULT_EVENTS_URL_VALUE.into()"]
|
||||
pub events_path: String,
|
||||
}
|
||||
|
|
@ -111,7 +113,7 @@ impl SlackCommandEventsListenerConfig {
|
|||
|
||||
#[derive(Debug, PartialEq, Clone, Builder)]
|
||||
pub struct SlackPushEventsListenerConfig {
|
||||
pub events_signing_secret: String,
|
||||
pub events_signing_secret: SlackSigningSecret,
|
||||
#[default = "SlackPushEventsListenerConfig::DEFAULT_EVENTS_URL_VALUE.into()"]
|
||||
pub events_path: String,
|
||||
}
|
||||
|
|
@ -122,7 +124,7 @@ impl SlackPushEventsListenerConfig {
|
|||
|
||||
#[derive(Debug, PartialEq, Clone, Builder)]
|
||||
pub struct SlackInteractionEventsListenerConfig {
|
||||
pub events_signing_secret: String,
|
||||
pub events_signing_secret: SlackSigningSecret,
|
||||
#[default = "SlackInteractionEventsListenerConfig::DEFAULT_EVENTS_URL_VALUE.into()"]
|
||||
pub events_path: String,
|
||||
}
|
||||
|
|
@ -133,8 +135,8 @@ impl SlackInteractionEventsListenerConfig {
|
|||
|
||||
#[derive(Debug, PartialEq, Clone, Builder)]
|
||||
pub struct SlackOAuthListenerConfig {
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub client_id: SlackClientId,
|
||||
pub client_secret: SlackClientSecret,
|
||||
pub bot_scope: String,
|
||||
pub redirect_callback_host: String,
|
||||
#[default = "SlackOAuthListenerConfig::DEFAULT_INSTALL_PATH_VALUE.into()"]
|
||||
|
|
@ -158,11 +160,15 @@ impl SlackOAuthListenerConfig {
|
|||
|
||||
pub const OAUTH_AUTHORIZE_URL_VALUE: &'static str = "https://slack.com/oauth/v2/authorize";
|
||||
|
||||
pub fn to_redirect_url(&self) -> String {
|
||||
format!(
|
||||
"{}{}",
|
||||
&self.redirect_callback_host, &self.redirect_callback_path
|
||||
pub fn to_redirect_url(&self) -> ClientResult<Url> {
|
||||
Url::parse(
|
||||
format!(
|
||||
"{}{}",
|
||||
&self.redirect_callback_host, &self.redirect_callback_path
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use ring::hmac;
|
||||
use rsb_derive::Builder;
|
||||
use rvstruct::*;
|
||||
use slack_morphism_models::SlackSigningSecret;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct SlackEventSignatureVerifier {
|
||||
secret_len: usize,
|
||||
key: hmac::Key,
|
||||
|
|
@ -13,11 +15,11 @@ impl SlackEventSignatureVerifier {
|
|||
pub const SLACK_SIGNED_HASH_HEADER: &'static str = "x-slack-signature";
|
||||
pub const SLACK_SIGNED_TIMESTAMP: &'static str = "x-slack-request-timestamp";
|
||||
|
||||
pub fn new(secret: &str) -> Self {
|
||||
let secret_bytes = secret.as_bytes();
|
||||
pub fn new(secret: &SlackSigningSecret) -> Self {
|
||||
let secret_bytes = secret.value().as_bytes();
|
||||
SlackEventSignatureVerifier {
|
||||
secret_len: secret_bytes.len(),
|
||||
key: hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes()),
|
||||
key: hmac::Key::new(hmac::HMAC_SHA256, secret_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +145,7 @@ fn check_signature_success() {
|
|||
ring::rand::generate(&rng).unwrap().expose();
|
||||
let key_str: String = hex::encode(key_value);
|
||||
|
||||
let verifier = SlackEventSignatureVerifier::new(&key_str);
|
||||
let verifier = SlackEventSignatureVerifier::new(&key_str.to_string().into());
|
||||
|
||||
const TEST_BODY: &'static str = "test-body";
|
||||
const TEST_TS: &'static str = "test-ts";
|
||||
|
|
@ -167,7 +169,7 @@ fn test_precoded_data() {
|
|||
const TEST_BODY: &'static str = "test-body";
|
||||
const TEST_TS: &'static str = "test-ts";
|
||||
|
||||
let verifier = SlackEventSignatureVerifier::new(TEST_SECRET);
|
||||
let verifier = SlackEventSignatureVerifier::new(&TEST_SECRET.to_string().into());
|
||||
|
||||
match verifier.verify(TEST_HASH, TEST_BODY, TEST_TS) {
|
||||
Ok(_) => {}
|
||||
|
|
@ -179,7 +181,11 @@ fn test_precoded_data() {
|
|||
|
||||
#[test]
|
||||
fn check_empty_secret_error_test() {
|
||||
match SlackEventSignatureVerifier::new("").verify("test-hash", "test-body", "test-ts") {
|
||||
match SlackEventSignatureVerifier::new(&"".to_string().into()).verify(
|
||||
"test-hash",
|
||||
"test-body",
|
||||
"test-ts",
|
||||
) {
|
||||
Err(SlackEventSignatureVerifierError::CryptoInitError(ref err)) => {
|
||||
assert!(!err.message.is_empty())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ use rvstruct::ValueStruct;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
|
||||
use slack_morphism_models::SlackTeamId;
|
||||
|
||||
// Re-exports for backward compatibility
|
||||
pub use slack_morphism_models::SlackApiTokenScope;
|
||||
use slack_morphism_models::{SlackApiTokenScope, SlackTeamId};
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct SlackApiTokenValue(pub String);
|
||||
|
|
|
|||
|
|
@ -97,23 +97,23 @@ async fn test_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
|
||||
let oauth_listener_config = Arc::new(SlackOAuthListenerConfig::new(
|
||||
config_env_var("SLACK_CLIENT_ID")?,
|
||||
config_env_var("SLACK_CLIENT_SECRET")?,
|
||||
config_env_var("SLACK_CLIENT_ID")?.into(),
|
||||
config_env_var("SLACK_CLIENT_SECRET")?.into(),
|
||||
config_env_var("SLACK_BOT_SCOPE")?,
|
||||
config_env_var("SLACK_REDIRECT_HOST")?,
|
||||
));
|
||||
|
||||
let push_events_config = Arc::new(SlackPushEventsListenerConfig::new(config_env_var(
|
||||
"SLACK_SIGNING_SECRET",
|
||||
)?));
|
||||
|
||||
let interactions_events_config = Arc::new(SlackInteractionEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?,
|
||||
let push_events_config = Arc::new(SlackPushEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let command_events_config = Arc::new(SlackCommandEventsListenerConfig::new(config_env_var(
|
||||
"SLACK_SIGNING_SECRET",
|
||||
)?));
|
||||
let interactions_events_config = Arc::new(SlackInteractionEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let command_events_config = Arc::new(SlackCommandEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let listener_environment = Arc::new(
|
||||
SlackClientEventsListenerEnvironment::new(client.clone())
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use futures::future::{BoxFuture, FutureExt};
|
|||
use hyper::body::*;
|
||||
use hyper::client::connect::Connect;
|
||||
use hyper::{Method, Request, Response};
|
||||
use rvstruct::*;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use tracing::*;
|
||||
|
|
@ -22,9 +23,12 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
let full_uri = SlackClientHttpApiUri::create_url_with_params(
|
||||
SlackOAuthListenerConfig::OAUTH_AUTHORIZE_URL_VALUE,
|
||||
&vec![
|
||||
("client_id", Some(&config.client_id)),
|
||||
("client_id", Some(config.client_id.value())),
|
||||
("scope", Some(&config.bot_scope)),
|
||||
("redirect_uri", Some(&config.to_redirect_url())),
|
||||
(
|
||||
"redirect_uri",
|
||||
Some(config.to_redirect_url()?.as_str().to_string()).as_ref(),
|
||||
),
|
||||
],
|
||||
);
|
||||
debug!("Redirecting to Slack OAuth authorize: {}", &full_uri);
|
||||
|
|
@ -55,7 +59,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
client_secret: config.client_secret.clone().into(),
|
||||
code: code.into(),
|
||||
})
|
||||
.with_redirect_uri(config.to_redirect_url()),
|
||||
.with_redirect_uri(config.to_redirect_url()?),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
|
|
|||
|
|
@ -109,9 +109,15 @@ pub struct SlackCommandId(pub String);
|
|||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct SlackClientId(pub String);
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct SlackClientSecret(pub String);
|
||||
|
||||
impl fmt::Debug for SlackClientSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SlackClientSecret(len:{})", self.value().len())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct SlackApiTokenScope(pub String);
|
||||
|
||||
|
|
@ -124,9 +130,15 @@ impl fmt::Debug for SlackVerificationToken {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct SlackSigningSecret(pub String);
|
||||
|
||||
impl fmt::Debug for SlackSigningSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SlackSigningSecret(len:{})", self.value().len())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct EmailAddress(pub String);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue