Social OAuth Integrations
Connect social platforms for authentication and content publishing.
Overview
Olympus Cloud supports OAuth integration with major social platforms for two purposes:
- Social Login - Allow users to sign in with existing social accounts
- Content Publishing - Enable creators to publish to social channels
| Platform | Login | Publishing | API Version |
|---|---|---|---|
| Yes | Yes | v2 | |
| Yes | Yes (Business) | Graph API | |
| YouTube | Yes | Yes | Data API v3 |
| X (Twitter) | Yes | Yes | v2 |
| Yes | Yes | Graph API | |
| TikTok | No | Yes | v2 |
LinkedIn Integration
Setup
# config/integrations/linkedin.yaml
linkedin:
client_id: "your-client-id"
client_secret: "your-client-secret"
redirect_uri: "https://api.olympuscloud.ai/oauth/linkedin/callback"
scopes:
- r_liteprofile
- r_emailaddress
- w_member_social
OAuth Flow
// src/integrations/linkedin/auth.rs
pub struct LinkedInAuth {
client_id: String,
client_secret: String,
redirect_uri: String,
}
impl LinkedInAuth {
pub fn get_authorization_url(&self, state: &str, scopes: &[&str]) -> String {
let scope = scopes.join("%20");
format!(
"https://www.linkedin.com/oauth/v2/authorization?\
response_type=code&\
client_id={}&\
redirect_uri={}&\
state={}&\
scope={}",
self.client_id,
urlencoding::encode(&self.redirect_uri),
state,
scope
)
}
pub async fn exchange_code(&self, code: &str) -> Result<LinkedInTokens> {
let client = reqwest::Client::new();
let response = client
.post("https://www.linkedin.com/oauth/v2/accessToken")
.form(&[
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", &self.redirect_uri),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
])
.send()
.await?;
response.json().await
}
pub async fn get_profile(&self, access_token: &str) -> Result<LinkedInProfile> {
let client = reqwest::Client::new();
let response = client
.get("https://api.linkedin.com/v2/me")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
response.json().await
}
pub async fn get_email(&self, access_token: &str) -> Result<String> {
let client = reqwest::Client::new();
let response = client
.get("https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
let email_response: LinkedInEmailResponse = response.json().await?;
Ok(email_response.elements[0].handle.email_address.clone())
}
}
Publish Content
// src/integrations/linkedin/publish.rs
pub async fn publish_post(
access_token: &str,
author_urn: &str,
content: &PostContent,
) -> Result<LinkedInPost> {
let client = reqwest::Client::new();
let payload = json!({
"author": author_urn,
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {
"text": content.text
},
"shareMediaCategory": "NONE"
}
},
"visibility": {
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
}
});
let response = client
.post("https://api.linkedin.com/v2/ugcPosts")
.header("Authorization", format!("Bearer {}", access_token))
.header("X-Restli-Protocol-Version", "2.0.0")
.json(&payload)
.send()
.await?;
response.json().await
}
pub async fn publish_with_image(
access_token: &str,
author_urn: &str,
content: &PostContent,
image_url: &str,
) -> Result<LinkedInPost> {
// First, register the image upload
let register_response = register_image_upload(access_token, author_urn).await?;
// Upload the image
upload_image(®ister_response.upload_url, image_url).await?;
// Create the post with the image
let payload = json!({
"author": author_urn,
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {
"text": content.text
},
"shareMediaCategory": "IMAGE",
"media": [{
"status": "READY",
"media": register_response.asset
}]
}
},
"visibility": {
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
}
});
let client = reqwest::Client::new();
let response = client
.post("https://api.linkedin.com/v2/ugcPosts")
.header("Authorization", format!("Bearer {}", access_token))
.json(&payload)
.send()
.await?;
response.json().await
}
Instagram Integration
Setup (Business Account)
# config/integrations/instagram.yaml
instagram:
app_id: "your-app-id"
app_secret: "your-app-secret"
redirect_uri: "https://api.olympuscloud.ai/oauth/instagram/callback"
scopes:
- instagram_basic
- instagram_content_publish
- pages_show_list
OAuth Flow
// src/integrations/instagram/auth.rs
pub struct InstagramAuth {
app_id: String,
app_secret: String,
redirect_uri: String,
}
impl InstagramAuth {
pub fn get_authorization_url(&self, state: &str) -> String {
format!(
"https://www.facebook.com/v18.0/dialog/oauth?\
client_id={}&\
redirect_uri={}&\
state={}&\
scope=instagram_basic,instagram_content_publish,pages_show_list",
self.app_id,
urlencoding::encode(&self.redirect_uri),
state
)
}
pub async fn exchange_code(&self, code: &str) -> Result<InstagramTokens> {
let client = reqwest::Client::new();
let response = client
.get("https://graph.facebook.com/v18.0/oauth/access_token")
.query(&[
("client_id", &self.app_id),
("redirect_uri", &self.redirect_uri),
("client_secret", &self.app_secret),
("code", &code.to_string()),
])
.send()
.await?;
response.json().await
}
pub async fn get_instagram_accounts(
&self,
access_token: &str,
) -> Result<Vec<InstagramAccount>> {
let client = reqwest::Client::new();
// Get Facebook pages
let pages_response = client
.get("https://graph.facebook.com/v18.0/me/accounts")
.query(&[("access_token", access_token)])
.send()
.await?;
let pages: PagesResponse = pages_response.json().await?;
let mut instagram_accounts = Vec::new();
for page in pages.data {
// Get Instagram account linked to page
let ig_response = client
.get(&format!(
"https://graph.facebook.com/v18.0/{}?fields=instagram_business_account",
page.id
))
.query(&[("access_token", &page.access_token)])
.send()
.await?;
if let Ok(ig) = ig_response.json::<InstagramAccountResponse>().await {
if let Some(ig_account) = ig.instagram_business_account {
instagram_accounts.push(ig_account);
}
}
}
Ok(instagram_accounts)
}
}
Publish Content
// src/integrations/instagram/publish.rs
pub async fn publish_image(
access_token: &str,
ig_user_id: &str,
image_url: &str,
caption: &str,
) -> Result<InstagramMedia> {
let client = reqwest::Client::new();
// Step 1: Create media container
let container_response = client
.post(&format!(
"https://graph.facebook.com/v18.0/{}/media",
ig_user_id
))
.query(&[
("image_url", image_url),
("caption", caption),
("access_token", access_token),
])
.send()
.await?;
let container: MediaContainer = container_response.json().await?;
// Step 2: Publish the container
let publish_response = client
.post(&format!(
"https://graph.facebook.com/v18.0/{}/media_publish",
ig_user_id
))
.query(&[
("creation_id", &container.id),
("access_token", access_token),
])
.send()
.await?;
publish_response.json().await
}
pub async fn publish_carousel(
access_token: &str,
ig_user_id: &str,
image_urls: &[&str],
caption: &str,
) -> Result<InstagramMedia> {
let client = reqwest::Client::new();
// Create containers for each image
let mut children_ids = Vec::new();
for url in image_urls {
let response = client
.post(&format!(
"https://graph.facebook.com/v18.0/{}/media",
ig_user_id
))
.query(&[
("image_url", *url),
("is_carousel_item", "true"),
("access_token", access_token),
])
.send()
.await?;
let container: MediaContainer = response.json().await?;
children_ids.push(container.id);
}
// Create carousel container
let carousel_response = client
.post(&format!(
"https://graph.facebook.com/v18.0/{}/media",
ig_user_id
))
.query(&[
("media_type", "CAROUSEL"),
("caption", caption),
("children", &children_ids.join(",")),
("access_token", access_token),
])
.send()
.await?;
let carousel: MediaContainer = carousel_response.json().await?;
// Publish carousel
let publish_response = client
.post(&format!(
"https://graph.facebook.com/v18.0/{}/media_publish",
ig_user_id
))
.query(&[
("creation_id", &carousel.id),
("access_token", access_token),
])
.send()
.await?;
publish_response.json().await
}
YouTube Integration
Setup
# config/integrations/youtube.yaml
youtube:
client_id: "your-client-id"
client_secret: "your-client-secret"
redirect_uri: "https://api.olympuscloud.ai/oauth/youtube/callback"
api_key: "your-api-key"
scopes:
- https://www.googleapis.com/auth/youtube.readonly
- https://www.googleapis.com/auth/youtube.upload
OAuth Flow
// src/integrations/youtube/auth.rs
pub struct YouTubeAuth {
client_id: String,
client_secret: String,
redirect_uri: String,
}
impl YouTubeAuth {
pub fn get_authorization_url(&self, state: &str) -> String {
let scopes = "https://www.googleapis.com/auth/youtube.readonly \
https://www.googleapis.com/auth/youtube.upload";
format!(
"https://accounts.google.com/o/oauth2/v2/auth?\
client_id={}&\
redirect_uri={}&\
response_type=code&\
scope={}&\
access_type=offline&\
state={}",
self.client_id,
urlencoding::encode(&self.redirect_uri),
urlencoding::encode(scopes),
state
)
}
pub async fn exchange_code(&self, code: &str) -> Result<YouTubeTokens> {
let client = reqwest::Client::new();
let response = client
.post("https://oauth2.googleapis.com/token")
.form(&[
("code", code),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
("redirect_uri", &self.redirect_uri),
("grant_type", "authorization_code"),
])
.send()
.await?;
response.json().await
}
pub async fn get_channel(&self, access_token: &str) -> Result<YouTubeChannel> {
let client = reqwest::Client::new();
let response = client
.get("https://www.googleapis.com/youtube/v3/channels")
.query(&[
("part", "snippet,statistics"),
("mine", "true"),
])
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
let channels: ChannelsResponse = response.json().await?;
channels.items.into_iter().next()
.ok_or(Error::NoChannel)
}
}
Upload Video
// src/integrations/youtube/upload.rs
pub async fn upload_video(
access_token: &str,
video_file: &Path,
metadata: &VideoMetadata,
) -> Result<YouTubeVideo> {
let client = reqwest::Client::new();
// Start resumable upload
let init_response = client
.post("https://www.googleapis.com/upload/youtube/v3/videos")
.query(&[
("uploadType", "resumable"),
("part", "snippet,status"),
])
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/json")
.json(&json!({
"snippet": {
"title": metadata.title,
"description": metadata.description,
"tags": metadata.tags,
"categoryId": metadata.category_id
},
"status": {
"privacyStatus": metadata.privacy_status,
"selfDeclaredMadeForKids": false
}
}))
.send()
.await?;
let upload_url = init_response.headers()
.get("Location")
.ok_or(Error::NoUploadUrl)?
.to_str()?;
// Upload video content
let video_bytes = tokio::fs::read(video_file).await?;
let upload_response = client
.put(upload_url)
.header("Content-Type", "video/*")
.body(video_bytes)
.send()
.await?;
upload_response.json().await
}
X (Twitter) Integration
Setup
# config/integrations/twitter.yaml
twitter:
client_id: "your-client-id"
client_secret: "your-client-secret"
redirect_uri: "https://api.olympuscloud.ai/oauth/twitter/callback"
bearer_token: "your-bearer-token"
OAuth 2.0 Flow
// src/integrations/twitter/auth.rs
pub struct TwitterAuth {
client_id: String,
client_secret: String,
redirect_uri: String,
}
impl TwitterAuth {
pub fn get_authorization_url(&self, state: &str, code_challenge: &str) -> String {
format!(
"https://twitter.com/i/oauth2/authorize?\
response_type=code&\
client_id={}&\
redirect_uri={}&\
scope=tweet.read%20tweet.write%20users.read%20offline.access&\
state={}&\
code_challenge={}&\
code_challenge_method=S256",
self.client_id,
urlencoding::encode(&self.redirect_uri),
state,
code_challenge
)
}
pub async fn exchange_code(
&self,
code: &str,
code_verifier: &str,
) -> Result<TwitterTokens> {
let client = reqwest::Client::new();
let response = client
.post("https://api.twitter.com/2/oauth2/token")
.basic_auth(&self.client_id, Some(&self.client_secret))
.form(&[
("code", code),
("grant_type", "authorization_code"),
("redirect_uri", &self.redirect_uri),
("code_verifier", code_verifier),
])
.send()
.await?;
response.json().await
}
}
Post Tweet
// src/integrations/twitter/publish.rs
pub async fn create_tweet(
access_token: &str,
text: &str,
) -> Result<Tweet> {
let client = reqwest::Client::new();
let response = client
.post("https://api.twitter.com/2/tweets")
.header("Authorization", format!("Bearer {}", access_token))
.json(&json!({ "text": text }))
.send()
.await?;
response.json().await
}
pub async fn create_tweet_with_media(
access_token: &str,
text: &str,
media_ids: &[String],
) -> Result<Tweet> {
let client = reqwest::Client::new();
let response = client
.post("https://api.twitter.com/2/tweets")
.header("Authorization", format!("Bearer {}", access_token))
.json(&json!({
"text": text,
"media": {
"media_ids": media_ids
}
}))
.send()
.await?;
response.json().await
}
Social Login Flow
Unified Login Handler
// src/auth/social_login.rs
pub async fn handle_social_login(
provider: SocialProvider,
code: &str,
state: &str,
) -> Result<AuthResult> {
// Exchange code for tokens
let (profile, email) = match provider {
SocialProvider::LinkedIn => {
let tokens = linkedin_auth.exchange_code(code).await?;
let profile = linkedin_auth.get_profile(&tokens.access_token).await?;
let email = linkedin_auth.get_email(&tokens.access_token).await?;
(SocialProfile::from(profile), email)
}
SocialProvider::Google => {
let tokens = google_auth.exchange_code(code).await?;
let profile = google_auth.get_profile(&tokens.access_token).await?;
(SocialProfile::from(profile), profile.email)
}
SocialProvider::Facebook => {
let tokens = facebook_auth.exchange_code(code).await?;
let profile = facebook_auth.get_profile(&tokens.access_token).await?;
(SocialProfile::from(profile), profile.email)
}
};
// Find or create user
let user = match user_service.find_by_email(&email).await? {
Some(user) => {
// Link social account if not already linked
social_service.link_account(&user.id, provider, &profile).await?;
user
}
None => {
// Create new user
user_service.create_from_social(provider, &profile, &email).await?
}
};
// Generate session
let session = auth_service.create_session(&user).await?;
Ok(AuthResult {
user,
access_token: session.access_token,
refresh_token: session.refresh_token,
})
}
Token Management
Refresh Tokens
pub async fn refresh_social_token(
user_id: &str,
provider: SocialProvider,
) -> Result<String> {
let stored_tokens = token_store.get(user_id, provider).await?;
let new_tokens = match provider {
SocialProvider::LinkedIn => {
linkedin_auth.refresh_token(&stored_tokens.refresh_token).await?
}
SocialProvider::YouTube => {
youtube_auth.refresh_token(&stored_tokens.refresh_token).await?
}
// ... other providers
};
token_store.update(user_id, provider, &new_tokens).await?;
Ok(new_tokens.access_token)
}
Token Encryption
pub fn encrypt_token(token: &str) -> Result<String> {
let cipher = Aes256Gcm::new(Key::from_slice(&ENCRYPTION_KEY));
let nonce = Nonce::from_slice(&generate_nonce());
let ciphertext = cipher.encrypt(nonce, token.as_bytes())?;
Ok(base64::encode([nonce.as_slice(), &ciphertext].concat()))
}
Error Codes
| Code | Description | Resolution |
|---|---|---|
OAUTH_DENIED | User denied authorization | Show cancellation message |
TOKEN_EXPIRED | Access token expired | Use refresh token |
RATE_LIMITED | API rate limit exceeded | Implement backoff |
ACCOUNT_NOT_LINKED | Social account not linked | Prompt to link |
PUBLISHING_FAILED | Content publish failed | Check content requirements |
Related Documentation
- Authentication API - OAuth flows
- Creator Studio - Content publishing
- Webhooks - Event notifications