From 25729ce492a6436b5206a148a261cef1e4c71153 Mon Sep 17 00:00:00 2001 From: Joren Date: Fri, 19 Jun 2026 21:00:02 +0200 Subject: [PATCH] fix: update Canvas OAuth flow --- internal/auth/auth.go | 134 +++++++++++++++++++++++++++++++------- internal/config/config.go | 4 +- 2 files changed, 112 insertions(+), 26 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index fbf5fc7..a6fe8b8 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,11 +1,17 @@ package auth import ( + "bufio" + "crypto/rand" + "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" + "io" "net/http" "net/url" "os" + "strings" "git.directme.in/Joren/CanvasArchiver/internal/config" "git.directme.in/Joren/CanvasArchiver/internal/models" @@ -24,28 +30,7 @@ func NewAuthenticator(client *http.Client) *Authenticator { func (a *Authenticator) GetAccessToken() (string, error) { creds, err := LoadCredentials() if err != nil { - - fmt.Println("--- Initial Canvas Login Required ---") - fmt.Printf("Visit: %s/login/oauth2/auth?client_id=%s&response_type=code&redirect_uri=%s\n", - config.BaseURL, config.ClientID, url.QueryEscape(config.RedirectURI)) - fmt.Print("Enter Code: ") - var code string - fmt.Scanln(&code) - - tr, err := a.doTokenRequest(url.Values{ - "grant_type": {"authorization_code"}, - "client_id": {config.ClientID}, - "client_secret": {config.ClientSecret}, - "redirect_uri": {config.RedirectURI}, - "code": {code}, - }) - if err != nil { - return "", err - } - - SaveCredentials(&models.Credentials{RefreshToken: tr.RefreshToken}) - fmt.Println("[+] Login successful.") - return tr.AccessToken, nil + return a.login() } fmt.Println("[*] Reusing saved refresh token...") @@ -54,11 +39,49 @@ func (a *Authenticator) GetAccessToken() (string, error) { "client_id": {config.ClientID}, "client_secret": {config.ClientSecret}, "refresh_token": {creds.RefreshToken}, + "redirect_uri": {config.RedirectURI}, + }) + if err != nil { + fmt.Printf("[!] Refresh failed: %v\n", err) + return a.login() + } + fmt.Println("[+] Session refreshed.") + return tr.AccessToken, nil +} + +func (a *Authenticator) login() (string, error) { + codeVerifier, codeChallenge, err := generatePKCEPair() + if err != nil { + return "", err + } + + fmt.Println("--- Initial Canvas Login Required ---") + fmt.Printf("Visit: %s\n", buildAuthURL(codeChallenge)) + fmt.Print("Enter Code or redirect URL: ") + input, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil && err != io.EOF { + return "", err + } + + code, err := extractCode(input) + if err != nil { + return "", err + } + + tr, err := a.doTokenRequest(url.Values{ + "grant_type": {"authorization_code"}, + "client_id": {config.ClientID}, + "client_secret": {config.ClientSecret}, + "redirect_uri": {config.RedirectURI}, + "code": {code}, + "code_verifier": {codeVerifier}, }) if err != nil { return "", err } - fmt.Println("[+] Session refreshed.") + + SaveCredentials(&models.Credentials{RefreshToken: tr.RefreshToken}) + fmt.Println("[+] Login successful.") return tr.AccessToken, nil } @@ -70,14 +93,75 @@ func (a *Authenticator) doTokenRequest(v url.Values) (*models.TokenResponse, err defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, fmt.Errorf("token request failed: %d", resp.StatusCode) + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("token request failed: %d: %s", resp.StatusCode, string(body)) } var tr models.TokenResponse - json.NewDecoder(resp.Body).Decode(&tr) + if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil { + return nil, err + } return &tr, nil } +func buildAuthURL(codeChallenge string) string { + u, _ := url.Parse(config.BaseURL + "/login/oauth2/auth") + q := u.Query() + q.Set("client_id", config.ClientID) + q.Set("response_type", "code") + q.Set("mobile", "1") + q.Set("purpose", config.OAuthPurpose) + q.Set("code_challenge", codeChallenge) + q.Set("code_challenge_method", "S256") + q.Set("redirect_uri", config.RedirectURI) + if config.AuthProvider != "" { + q.Set("authentication_provider", config.AuthProvider) + } + u.RawQuery = q.Encode() + return u.String() +} + +func generatePKCEPair() (string, string, error) { + randomBytes := make([]byte, 64) + if _, err := rand.Read(randomBytes); err != nil { + return "", "", err + } + + verifier := base64.RawURLEncoding.EncodeToString(randomBytes) + challengeBytes := sha256.Sum256([]byte(verifier)) + challenge := base64.RawURLEncoding.EncodeToString(challengeBytes[:]) + return verifier, challenge, nil +} + +func extractCode(input string) (string, error) { + input = strings.TrimSpace(input) + if input == "" { + return "", fmt.Errorf("empty authorization code") + } + + for _, field := range strings.Fields(input) { + if strings.Contains(field, "code=") { + input = field + break + } + } + + parsedURL, err := url.Parse(input) + if err == nil { + if code := parsedURL.Query().Get("code"); code != "" { + return code, nil + } + } + + if values, err := url.ParseQuery(input); err == nil { + if code := values.Get("code"); code != "" { + return code, nil + } + } + + return input, nil +} + func SaveCredentials(creds *models.Credentials) { data, _ := json.MarshalIndent(creds, "", " ") os.WriteFile(config.CredsFile, data, 0o644) diff --git a/internal/config/config.go b/internal/config/config.go index b91afa6..d7a1866 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,7 +4,9 @@ const ( BaseURL = "https://canvas.vub.be" ClientID = "170000000000044" ClientSecret = "3sxR3NtgXRfT9KdpWGAFQygq6O9RzLN021h2lAzhHUZEeSQ5XGV41Ddi5iutwW6f" - RedirectURI = "urn:ietf:wg:oauth:2.0:oob" + RedirectURI = "https://sso.canvaslms.com/canvas/login" + OAuthPurpose = "CanvasArchiver" + AuthProvider = "microsoft" CredsFile = "credentials.json" PanoptoID = "15" UserAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36"