# 使用 Gitea 登录：OAuth 2.0 接入指南（Golang）

**Gitea** 实例提供与平台绑定的 OAuth2 端点：授权地址、令牌地址都带**你的 Gitea 根 URL** 前缀。本文说明通用注册方式与 Go 中的 `oauth2.Endpoint` 拼法，与具体业务项目无关。

## 1. 协议与端点

对 Gitea 根地址 `BASE`（无尾斜杠，如 `https://git.example.com`）：

| 步骤 | URL |
|------|-----|
| 授权 | `{BASE}/login/oauth/authorize` |
| 换 token | `{BASE}/login/oauth/access_token` |
| 当前用户 API | `{BASE}/api/v1/user`（Header：`Authorization: token {access_token}`） |

Scope 常用：`read:user`（以你 Gitea 版本界面为准）。

官方概念说明见 Gitea 文档中的 OAuth2 / Applications 章节（不同版本路径可能为站点管理或用户 **Applications**）。

## 2. 在 Gitea 中注册应用

1. 在目标 Gitea 上创建 **OAuth2 应用** / **应用（OAuth2）**，填写 **Redirect URI**（你自有服务的回调，须 HTTPS 生产或本地开发约定）。  
2. 记录 **Client ID**、**Client Secret**。

**注意**：`redirect_uri` 在授权与换 token 两阶段须一致，且与 Gitea 中登记项一致。

## 3. Golang：`oauth2.Endpoint` 与示例

`golang.org/x/oauth2` 的 `Config.Endpoint` 需手动设为：

```go
endpoint := oauth2.Endpoint{
	AuthURL:  base + "/login/oauth/authorize",
	TokenURL: base + "/login/oauth/access_token",
}
```

调用 Gitea `GET /api/v1/user` 时，使用 **token** 头：`Authorization: token ` + `accessToken`（Gitea 传统写法；若你的版本支持 Bearer 以实际文档为准）。

## 4. 最小示例片段

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"

	"golang.org/x/oauth2"
)

func giteaEndpoint(base string) oauth2.Endpoint {
	base = trimRightSlash(base)
	return oauth2.Endpoint{
		AuthURL:  base + "/login/oauth/authorize",
		TokenURL: base + "/login/oauth/access_token",
	}
}

func trimRightSlash(s string) string {
	for len(s) > 0 && s[len(s)-1] == '/' {
		s = s[:len(s)-1]
	}
	return s
}

// 示例：构造 Config（BASE、ClientID、Secret、RedirectURL 从环境或配置读取）
func newGiteaConfig() *oauth2.Config {
	base := os.Getenv("GITEA_BASE_URL") // 如 https://git.example.com
	return &oauth2.Config{
		ClientID:     os.Getenv("GITEA_CLIENT_ID"),
		ClientSecret: os.Getenv("GITEA_CLIENT_SECRET"),
		RedirectURL:  os.Getenv("GITEA_REDIRECT_URL"),
		Scopes:       []string{"read:user"},
		Endpoint:     giteaEndpoint(base),
	}
}

type giteaUser struct {
	ID        int64  `json:"id"`
	Login     string `json:"login"`
	FullName  string `json:"full_name"`
	Email     string `json:"email"`
	AvatarURL string `json:"avatar_url"`
}

func fetchGiteaUser(ctx context.Context, base, access string) (*giteaUser, error) {
	base = trimRightSlash(base)
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+"/api/v1/user", nil)
	req.Header.Set("Authorization", "token "+access)
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	b, _ := io.ReadAll(res.Body)
	if res.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("gitea: %d %s", res.StatusCode, b)
	}
	var u giteaUser
	if err := json.Unmarshal(b, &u); err != nil {
		return nil, err
	}
	return &u, nil
}

```

在 `main` 中挂载 HTTP 路由：首页生成 `state` 并重定向到 `cfg.AuthCodeURL`；在 `/oauth/gitea/callback` 中 `cfg.Exchange` 后调用 `fetchGiteaUser` 即可。整体与 [github.md](github.md) 中示例同构，仅将 `Config.Endpoint` 换为 `giteaEndpoint`、将 GitHub `Bearer` 换为 Gitea 的 `token` 头。

## 5. 常见错误

- **401**：token 头格式或 API 版本与实例不一致。  
- **redirect_uri 不匹配**：授权与回调的 host/scheme/端口与 Gitea 中登记的不一致。  
- 多 Gitea 实例时：每个 **Client** 与 **base URL** 一一对应，勿混用。

## 6. 安全建议

- Secret 只放服务端。  
- 以 Gitea 返回的**数字 user id** 作为绑定主键更稳妥。
