package socialconnect import ( "context" "fmt" "net/http" "net/url" "time" ) const ( tiktokDefaultAuthorizeURL = "https://www.tiktok.com/v2/auth/authorize/" tiktokDefaultTokenURL = "https://open.tiktokapis.com/v2/oauth/token/" // user.info.basic reicht für Profil-Grunddaten; video.list für den // späteren Abruf veröffentlichter Videos (Beweissicherung). Mehr // Scopes verlangen als nötig verzögert nur den Audit, siehe // Paket-Kommentar. tiktokScope = "user.info.basic,video.list" ) // TikTokConnector implementiert Connector für TikTok Login Kit v2 (Web- // Flow — PKCE ist bei TikTok nur für Desktop/Mobile-Apps Pflicht, beim // Web-Flow schützt allein der state-Parameter gegen CSRF, siehe // TikTok-Dokumentation "Web"). type TikTokConnector struct { ClientKey string ClientSecret string RedirectURL string // Überschreibbar für Tests (Default: die echten TikTok-Endpunkte). AuthorizeURL string TokenURL string HTTPClient *http.Client } // NewTikTokConnector erstellt einen TikTokConnector mit den echten // TikTok-Endpunkten. func NewTikTokConnector(clientKey, clientSecret, redirectURL string) *TikTokConnector { return &TikTokConnector{ ClientKey: clientKey, ClientSecret: clientSecret, RedirectURL: redirectURL, AuthorizeURL: tiktokDefaultAuthorizeURL, TokenURL: tiktokDefaultTokenURL, HTTPClient: http.DefaultClient, } } func (c *TikTokConnector) Platform() string { return "tiktok" } func (c *TikTokConnector) AuthorizationURL(state string) string { v := url.Values{ "client_key": {c.ClientKey}, "redirect_uri": {c.RedirectURL}, "scope": {tiktokScope}, "response_type": {"code"}, "state": {state}, } return c.AuthorizeURL + "?" + v.Encode() } func (c *TikTokConnector) Exchange(ctx context.Context, code string) (Token, error) { form := url.Values{ "client_key": {c.ClientKey}, "client_secret": {c.ClientSecret}, "code": {code}, "grant_type": {"authorization_code"}, "redirect_uri": {c.RedirectURL}, } var resp struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` OpenID string `json:"open_id"` Error string `json:"error"` ErrorDescription string `json:"error_description"` } if err := postForm(ctx, c.HTTPClient, c.TokenURL, form, &resp); err != nil { return Token{}, fmt.Errorf("socialconnect: tiktok code exchange: %w", err) } if resp.Error != "" { return Token{}, fmt.Errorf("socialconnect: tiktok code exchange: %s: %s", resp.Error, resp.ErrorDescription) } if resp.AccessToken == "" { return Token{}, fmt.Errorf("socialconnect: tiktok code exchange: kein access_token in der Antwort") } return Token{ AccessToken: resp.AccessToken, RefreshToken: resp.RefreshToken, ExpiresAt: time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second), PlatformUserID: resp.OpenID, }, nil }