Slack uses the OAuth 2.0 protocol to allow your application to access Slack workspaces on behalf of users. There are two main flows: one for custom Slack apps installed in workspaces, and another for Sign in with Slack.
Understanding Slack OAuth Flows
1. App Installation Flow
Used when users install your Slack app into their workspace. This grants your app permissions to access workspace resources.
Key Steps:
- Redirect user to Slack's authorization URL
- User approves permissions and is redirected back to your app
- Exchange the temporary code for an access token
- Store the token for making API calls
2. Sign in with Slack
Allows users to authenticate using their Slack identity, similar to "Login with Google/Facebook."
Implementation Guide
Let's implement the Sign in with Slack flow, which is more common for web applications.
1. Project Dependencies (pom.xml)
<dependencies> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security OAuth2 Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <!-- Spring Security Config --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- HTTP Client for manual implementation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>
2. Slack App Configuration
First, create a Slack app at api.slack.com/apps:
- Go to OAuth & Permissions
- Add redirect URLs:
http://localhost:8080/login/oauth2/code/slack - Configure user scopes (e.g.,
identity.basic,identity.email,identity.avatar)
3. Application Configuration (application.yml)
spring:
security:
oauth2:
client:
registration:
slack:
client-id: ${SLACK_CLIENT_ID}
client-secret: ${SLACK_CLIENT_SECRET}
scope:
- openid
- profile
- email
- users:read
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-name: Slack
provider:
slack:
authorization-uri: https://slack.com/oauth/v2/authorize
token-uri: https://slack.com/api/oauth.v2.access
user-info-uri: https://slack.com/api/users.identity
user-name-attribute: sub
# Server configuration
server:
port: 8080
# Your Slack app credentials
slack:
client:
id: ${SLACK_CLIENT_ID}
secret: ${SLACK_CLIENT_SECRET}
4. Security Configuration (Auto-Configuration Approach)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll()
);
return http.build();
}
}
5. Manual Implementation (More Control)
If you need more control over the OAuth flow, here's a manual implementation:
Controller:
@RestController
@RequestMapping("/auth")
public class SlackAuthController {
private final SlackOAuthService slackOAuthService;
public SlackAuthController(SlackOAuthService slackOAuthService) {
this.slackOAuthService = slackOAuthService;
}
@GetMapping("/slack")
public String initiateSlackAuth() {
String state = slackOAuthService.generateState();
String nonce = slackOAuthService.generateNonce();
// Store state and nonce in session/cache for validation
return "redirect:" + slackOAuthService.buildAuthorizationUrl(state, nonce);
}
@GetMapping("/slack/callback")
public ResponseEntity<?> handleSlackCallback(
@RequestParam String code,
@RequestParam String state,
HttpSession session) {
try {
// Validate state parameter
if (!slackOAuthService.validateState(state, session)) {
return ResponseEntity.badRequest().body("Invalid state parameter");
}
SlackUserInfo userInfo = slackOAuthService.exchangeCodeForUserInfo(code);
// Here you would typically:
// 1. Check if user exists in your database
// 2. Create user if not exists
// 3. Create session/JWT
// 4. Log the user in
return ResponseEntity.ok().body(
Map.of("message", "Login successful", "user", userInfo)
);
} catch (Exception e) {
return ResponseEntity.badRequest().body(
Map.of("error", "Authentication failed: " + e.getMessage())
);
}
}
}
Service Layer:
@Service
public class SlackOAuthService {
@Value("${slack.client.id}")
private String clientId;
@Value("${slack.client.secret}")
private String clientSecret;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
private final WebClient webClient;
public SlackOAuthService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.build();
}
public String buildAuthorizationUrl(String state, String nonce) {
return UriComponentsBuilder.fromHttpUrl("https://slack.com/oauth/v2/authorize")
.queryParam("client_id", clientId)
.queryParam("scope", "openid profile email")
.queryParam("redirect_uri", baseUrl + "/auth/slack/callback")
.queryParam("state", state)
.queryParam("nonce", nonce)
.build()
.toUriString();
}
public SlackUserInfo exchangeCodeForUserInfo(String code) {
// Exchange authorization code for access token
SlackTokenResponse tokenResponse = webClient.post()
.uri("https://slack.com/api/oauth.v2.access")
.header("Content-Type", "application/x-www-form-urlencoded")
.body(BodyInserters.fromFormData("client_id", clientId)
.with("client_secret", clientSecret)
.with("code", code)
.with("redirect_uri", baseUrl + "/auth/slack/callback"))
.retrieve()
.bodyToMono(SlackTokenResponse.class)
.block();
if (tokenResponse == null || !tokenResponse.isOk()) {
throw new RuntimeException("Failed to get access token: " +
(tokenResponse != null ? tokenResponse.getError() : "Unknown error"));
}
// Get user info using the access token
return webClient.get()
.uri("https://slack.com/api/users.identity")
.header("Authorization", "Bearer " + tokenResponse.getAccessToken())
.retrieve()
.bodyToMono(SlackUserInfo.class)
.block();
}
public String generateState() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
public String generateNonce() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
random.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
public boolean validateState(String state, HttpSession session) {
// Implement state validation logic
// Typically you'd compare with state stored in session
return true; // Simplified for example
}
}
Data Transfer Objects:
// Token Response DTO
public class SlackTokenResponse {
private boolean ok;
private String accessToken;
private String tokenType;
private String scope;
private String error;
// Getters and Setters
public boolean isOk() { return ok; }
public void setOk(boolean ok) { this.ok = ok; }
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public String getTokenType() { return tokenType; }
public void setTokenType(String tokenType) { this.tokenType = tokenType; }
public String getScope() { return scope; }
public void setScope(String scope) { this.scope = scope; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
}
// User Info DTO
public class SlackUserInfo {
private boolean ok;
private SlackUser user;
private SlackTeam team;
private String error;
// Getters and Setters
public boolean isOk() { return ok; }
public void setOk(boolean ok) { this.ok = ok; }
public SlackUser getUser() { return user; }
public void setUser(SlackUser user) { this.user = user; }
public SlackTeam getTeam() { return team; }
public void setTeam(SlackTeam team) { this.team = team; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public static class SlackUser {
private String id;
private String name;
private String email;
private String image192;
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getImage192() { return image192; }
public void setImage192(String image192) { this.image192 = image192; }
}
public static class SlackTeam {
private String id;
private String name;
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
}
6. Frontend Integration
Login Button:
<!DOCTYPE html> <html> <head> <title>Login with Slack</title> </head> <body> <div class="container"> <h1>Welcome</h1> <a href="/auth/slack" class="slack-login-button"> <img src="/slack-button.png" alt="Sign in with Slack"> </a> <!-- Or with Spring Security auto-configuration --> <a href="/oauth2/authorization/slack">Sign in with Slack</a> </div> </body> </html>
Key Security Considerations
- State Parameter: Always use and validate the
stateparameter to prevent CSRF attacks. - HTTPS: Always use HTTPS in production for OAuth callbacks.
- Token Storage: Store access tokens securely (encrypted) if you need to persist them.
- Scope Minimization: Request only the scopes your application actually needs.
- Error Handling: Implement proper error handling for various OAuth failure scenarios.
Testing the Implementation
- Set environment variables:
export SLACK_CLIENT_ID=your-client-id export SLACK_CLIENT_SECRET=your-client-secret
- Run your Spring Boot application
- Navigate to
http://localhost:8080 - Click the "Sign in with Slack" button
- You should be redirected to Slack for authorization
- After approving, you'll be redirected back to your app with user information
This implementation provides a solid foundation for Slack OAuth integration that you can extend based on your specific requirements, whether you're building a Slack app or implementing Slack-based authentication for your web application.