authentik.sources.oauth.types.slack

Slack OAuth Views

  1"""Slack OAuth Views"""
  2
  3from typing import Any
  4
  5from django.http import Http404
  6
  7from authentik.sources.oauth.clients.oauth2 import OAuth2Client
  8from authentik.sources.oauth.models import OAuthSource
  9from authentik.sources.oauth.types.registry import SourceType, registry
 10from authentik.sources.oauth.views.callback import OAuthCallback
 11from authentik.sources.oauth.views.redirect import OAuthRedirect
 12
 13
 14class SlackOAuthClient(OAuth2Client):
 15    """Slack OAuth2 Client that handles Slack's nested token response.
 16
 17    Slack's oauth.v2.access returns tokens in a nested structure:
 18    {
 19        "ok": true,
 20        "access_token": "xoxb-...",  # bot token
 21        "refresh_token": "xoxe-1-...",  # bot refresh token (if rotation enabled)
 22        "authed_user": {
 23            "id": "U1234",
 24            "scope": "...",
 25            "access_token": "xoxp-...",  # user token
 26            "refresh_token": "xoxe-1-...",  # user refresh token (if rotation enabled)
 27            "token_type": "user",
 28            "expires_in": 43200
 29        }
 30    }
 31
 32    For user scopes (like admin for SCIM), we need the authed_user token.
 33    """
 34
 35    def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
 36        """Fetch access token and normalize Slack's nested response."""
 37        token = super().get_access_token(**request_kwargs)
 38        if token is None or "error" in token:
 39            return token
 40
 41        # If we have authed_user with access_token, use that (user token)
 42        # If authed_user isn't there, then we were given a bot token
 43        if "authed_user" in token and "access_token" in token.get("authed_user", {}):
 44            authed_user = token["authed_user"]
 45            token["access_token"] = authed_user["access_token"]
 46
 47            if "refresh_token" in authed_user:
 48                token["refresh_token"] = authed_user["refresh_token"]
 49            if "expires_in" in authed_user:
 50                token["expires_in"] = authed_user["expires_in"]
 51            token["id"] = authed_user.get("id")
 52
 53        # Slack returns "user", but API expects Bearer
 54        # not a password, OAuth token type
 55        token["token_type"] = "Bearer"  # nosec
 56
 57        return token
 58
 59
 60class SlackOAuthRedirect(OAuthRedirect):
 61    """Slack OAuth2 Redirect
 62
 63    Slack uses two separate scope parameters:
 64    - scope: Bot token scopes (xoxb- tokens)
 65    - user_scope: User token scopes (xoxp- tokens)
 66
 67    For user authentication and SCIM,
 68    we need scopes in user_scope, not scope.
 69    """
 70
 71    def get_additional_parameters(self, source):
 72        # Start with base user scopes for authentication
 73        user_scopes = ["openid", "email", "profile"]
 74
 75        # Add any additional scopes from the source config to user_scope
 76        # (not to scope, which is for bot tokens)
 77        if source.additional_scopes:
 78            additional = source.additional_scopes
 79            if additional.startswith("*"):
 80                additional = additional[1:]
 81            user_scopes.extend(additional.split())
 82
 83        return {
 84            "scope": [],
 85            "user_scope": user_scopes,
 86        }
 87
 88    def get_redirect_url(self, **kwargs) -> str:
 89        """Build redirect URL with Slack-specific scope handling.
 90
 91        Slack uses two separate scope parameters:
 92        - scope: Bot token scopes (xoxb- tokens)
 93        - user_scope: User token scopes (xoxp- tokens)
 94
 95        The base class adds additional_scopes to 'scope', but Slack needs them
 96        in 'user_scope'. We override completely to handle this properly.
 97        """
 98
 99        slug = kwargs.get("source_slug", "")
100        try:
101            source: OAuthSource = OAuthSource.objects.get(slug=slug)
102        except OAuthSource.DoesNotExist:
103            raise Http404(f"Unknown OAuth source '{slug}'.") from None
104        if not source.enabled:
105            raise Http404(f"source {slug} is not enabled.")
106
107        client = self.get_client(source, callback=self.get_callback_url(source))
108        # get_additional_parameters handles all scopes for Slack (both scope and user_scope)
109        params = self.get_additional_parameters(source)
110        params.update(self._try_login_hint_extract())
111        return client.get_redirect_url(params)
112
113
114class SlackOAuth2Callback(OAuthCallback):
115    """Slack OAuth2 Callback"""
116
117    client_class = SlackOAuthClient
118
119    def get_user_id(self, info: dict[str, Any]) -> str | None:
120        """Return unique identifier from Slack profile info."""
121        return info.get("sub")
122
123
124@registry.register()
125class SlackType(SourceType):
126    """Slack Type definition"""
127
128    callback_view = SlackOAuth2Callback
129    redirect_view = SlackOAuthRedirect
130    verbose_name = "Slack"
131    name = "slack"
132
133    authorization_url = "https://slack.com/oauth/v2/authorize"
134    access_token_url = "https://slack.com/api/oauth.v2.access"  # nosec
135    profile_url = "https://slack.com/api/openid.connect.userInfo"
136
137    def get_base_user_properties(self, source, info: dict[str, Any], **kwargs) -> dict[str, Any]:
138        return {
139            "username": info.get("name"),
140            "email": info.get("email"),
141            "name": info.get("name"),
142        }
class SlackOAuthClient(authentik.sources.oauth.clients.oauth2.OAuth2Client):
15class SlackOAuthClient(OAuth2Client):
16    """Slack OAuth2 Client that handles Slack's nested token response.
17
18    Slack's oauth.v2.access returns tokens in a nested structure:
19    {
20        "ok": true,
21        "access_token": "xoxb-...",  # bot token
22        "refresh_token": "xoxe-1-...",  # bot refresh token (if rotation enabled)
23        "authed_user": {
24            "id": "U1234",
25            "scope": "...",
26            "access_token": "xoxp-...",  # user token
27            "refresh_token": "xoxe-1-...",  # user refresh token (if rotation enabled)
28            "token_type": "user",
29            "expires_in": 43200
30        }
31    }
32
33    For user scopes (like admin for SCIM), we need the authed_user token.
34    """
35
36    def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
37        """Fetch access token and normalize Slack's nested response."""
38        token = super().get_access_token(**request_kwargs)
39        if token is None or "error" in token:
40            return token
41
42        # If we have authed_user with access_token, use that (user token)
43        # If authed_user isn't there, then we were given a bot token
44        if "authed_user" in token and "access_token" in token.get("authed_user", {}):
45            authed_user = token["authed_user"]
46            token["access_token"] = authed_user["access_token"]
47
48            if "refresh_token" in authed_user:
49                token["refresh_token"] = authed_user["refresh_token"]
50            if "expires_in" in authed_user:
51                token["expires_in"] = authed_user["expires_in"]
52            token["id"] = authed_user.get("id")
53
54        # Slack returns "user", but API expects Bearer
55        # not a password, OAuth token type
56        token["token_type"] = "Bearer"  # nosec
57
58        return token

Slack OAuth2 Client that handles Slack's nested token response.

Slack's oauth.v2.access returns tokens in a nested structure: { "ok": true, "access_token": "xoxb-...", # bot token "refresh_token": "xoxe-1-...", # bot refresh token (if rotation enabled) "authed_user": { "id": "U1234", "scope": "...", "access_token": "xoxp-...", # user token "refresh_token": "xoxe-1-...", # user refresh token (if rotation enabled) "token_type": "user", "expires_in": 43200 } }

For user scopes (like admin for SCIM), we need the authed_user token.

def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
36    def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
37        """Fetch access token and normalize Slack's nested response."""
38        token = super().get_access_token(**request_kwargs)
39        if token is None or "error" in token:
40            return token
41
42        # If we have authed_user with access_token, use that (user token)
43        # If authed_user isn't there, then we were given a bot token
44        if "authed_user" in token and "access_token" in token.get("authed_user", {}):
45            authed_user = token["authed_user"]
46            token["access_token"] = authed_user["access_token"]
47
48            if "refresh_token" in authed_user:
49                token["refresh_token"] = authed_user["refresh_token"]
50            if "expires_in" in authed_user:
51                token["expires_in"] = authed_user["expires_in"]
52            token["id"] = authed_user.get("id")
53
54        # Slack returns "user", but API expects Bearer
55        # not a password, OAuth token type
56        token["token_type"] = "Bearer"  # nosec
57
58        return token

Fetch access token and normalize Slack's nested response.

class SlackOAuthRedirect(authentik.sources.oauth.views.redirect.OAuthRedirect):
 61class SlackOAuthRedirect(OAuthRedirect):
 62    """Slack OAuth2 Redirect
 63
 64    Slack uses two separate scope parameters:
 65    - scope: Bot token scopes (xoxb- tokens)
 66    - user_scope: User token scopes (xoxp- tokens)
 67
 68    For user authentication and SCIM,
 69    we need scopes in user_scope, not scope.
 70    """
 71
 72    def get_additional_parameters(self, source):
 73        # Start with base user scopes for authentication
 74        user_scopes = ["openid", "email", "profile"]
 75
 76        # Add any additional scopes from the source config to user_scope
 77        # (not to scope, which is for bot tokens)
 78        if source.additional_scopes:
 79            additional = source.additional_scopes
 80            if additional.startswith("*"):
 81                additional = additional[1:]
 82            user_scopes.extend(additional.split())
 83
 84        return {
 85            "scope": [],
 86            "user_scope": user_scopes,
 87        }
 88
 89    def get_redirect_url(self, **kwargs) -> str:
 90        """Build redirect URL with Slack-specific scope handling.
 91
 92        Slack uses two separate scope parameters:
 93        - scope: Bot token scopes (xoxb- tokens)
 94        - user_scope: User token scopes (xoxp- tokens)
 95
 96        The base class adds additional_scopes to 'scope', but Slack needs them
 97        in 'user_scope'. We override completely to handle this properly.
 98        """
 99
100        slug = kwargs.get("source_slug", "")
101        try:
102            source: OAuthSource = OAuthSource.objects.get(slug=slug)
103        except OAuthSource.DoesNotExist:
104            raise Http404(f"Unknown OAuth source '{slug}'.") from None
105        if not source.enabled:
106            raise Http404(f"source {slug} is not enabled.")
107
108        client = self.get_client(source, callback=self.get_callback_url(source))
109        # get_additional_parameters handles all scopes for Slack (both scope and user_scope)
110        params = self.get_additional_parameters(source)
111        params.update(self._try_login_hint_extract())
112        return client.get_redirect_url(params)

Slack OAuth2 Redirect

Slack uses two separate scope parameters:

  • scope: Bot token scopes (xoxb- tokens)
  • user_scope: User token scopes (xoxp- tokens)

For user authentication and SCIM, we need scopes in user_scope, not scope.

def get_additional_parameters(self, source):
72    def get_additional_parameters(self, source):
73        # Start with base user scopes for authentication
74        user_scopes = ["openid", "email", "profile"]
75
76        # Add any additional scopes from the source config to user_scope
77        # (not to scope, which is for bot tokens)
78        if source.additional_scopes:
79            additional = source.additional_scopes
80            if additional.startswith("*"):
81                additional = additional[1:]
82            user_scopes.extend(additional.split())
83
84        return {
85            "scope": [],
86            "user_scope": user_scopes,
87        }

Return additional redirect parameters for this source.

def get_redirect_url(self, **kwargs) -> str:
 89    def get_redirect_url(self, **kwargs) -> str:
 90        """Build redirect URL with Slack-specific scope handling.
 91
 92        Slack uses two separate scope parameters:
 93        - scope: Bot token scopes (xoxb- tokens)
 94        - user_scope: User token scopes (xoxp- tokens)
 95
 96        The base class adds additional_scopes to 'scope', but Slack needs them
 97        in 'user_scope'. We override completely to handle this properly.
 98        """
 99
100        slug = kwargs.get("source_slug", "")
101        try:
102            source: OAuthSource = OAuthSource.objects.get(slug=slug)
103        except OAuthSource.DoesNotExist:
104            raise Http404(f"Unknown OAuth source '{slug}'.") from None
105        if not source.enabled:
106            raise Http404(f"source {slug} is not enabled.")
107
108        client = self.get_client(source, callback=self.get_callback_url(source))
109        # get_additional_parameters handles all scopes for Slack (both scope and user_scope)
110        params = self.get_additional_parameters(source)
111        params.update(self._try_login_hint_extract())
112        return client.get_redirect_url(params)

Build redirect URL with Slack-specific scope handling.

Slack uses two separate scope parameters:

  • scope: Bot token scopes (xoxb- tokens)
  • user_scope: User token scopes (xoxp- tokens)

The base class adds additional_scopes to 'scope', but Slack needs them in 'user_scope'. We override completely to handle this properly.

class SlackOAuth2Callback(authentik.sources.oauth.views.callback.OAuthCallback):
115class SlackOAuth2Callback(OAuthCallback):
116    """Slack OAuth2 Callback"""
117
118    client_class = SlackOAuthClient
119
120    def get_user_id(self, info: dict[str, Any]) -> str | None:
121        """Return unique identifier from Slack profile info."""
122        return info.get("sub")

Slack OAuth2 Callback

client_class = <class 'SlackOAuthClient'>
def get_user_id(self, info: dict[str, typing.Any]) -> str | None:
120    def get_user_id(self, info: dict[str, Any]) -> str | None:
121        """Return unique identifier from Slack profile info."""
122        return info.get("sub")

Return unique identifier from Slack profile info.

@registry.register()
class SlackType(authentik.sources.oauth.types.registry.SourceType):
125@registry.register()
126class SlackType(SourceType):
127    """Slack Type definition"""
128
129    callback_view = SlackOAuth2Callback
130    redirect_view = SlackOAuthRedirect
131    verbose_name = "Slack"
132    name = "slack"
133
134    authorization_url = "https://slack.com/oauth/v2/authorize"
135    access_token_url = "https://slack.com/api/oauth.v2.access"  # nosec
136    profile_url = "https://slack.com/api/openid.connect.userInfo"
137
138    def get_base_user_properties(self, source, info: dict[str, Any], **kwargs) -> dict[str, Any]:
139        return {
140            "username": info.get("name"),
141            "email": info.get("email"),
142            "name": info.get("name"),
143        }

Slack Type definition

callback_view = <class 'SlackOAuth2Callback'>
redirect_view = <class 'SlackOAuthRedirect'>
verbose_name = 'Slack'
name = 'slack'
authorization_url = 'https://slack.com/oauth/v2/authorize'
access_token_url = 'https://slack.com/api/oauth.v2.access'
profile_url = 'https://slack.com/api/openid.connect.userInfo'
def get_base_user_properties( self, source, info: dict[str, typing.Any], **kwargs) -> dict[str, typing.Any]:
138    def get_base_user_properties(self, source, info: dict[str, Any], **kwargs) -> dict[str, Any]:
139        return {
140            "username": info.get("name"),
141            "email": info.get("email"),
142            "name": info.get("name"),
143        }

Get base user properties for enrollment/update