OAuth2 Service Provider
Time может выступать в качестве поставщика услуг OAuth2, что позволяет внешним приложениям получать доступ к ресурсам Time от имени пользователей.
Регистрация OAuth приложения в Time
Чтобы настроить Time в качестве поставщика услуг OAuth2, необходимо выполнить следующие шаги:
- От системного администратора в системной консоли включить опцию
"Включить поставщика услуг OAuth 2.0"
в разделеНастройки
->Интеграции
. - В разделе
Интеграции
->OAuth 2.0 приложения
нажав на кнопку"Подключить OAuth 2.0 приложение"
зарегестрировать новое OAuth приложение.- В поле
Домашняя страница
указать адрес приложения. - В поле
URL обратного вызова
указать адрес, на который будет перенаправлен пользователь после авторизации в Time.
- В поле
- В открывшемся окне заполнить информацию о приложении и нажать кнопку
Сохранить
. - После сохранения будут предоставлены
Client ID
иClient Secret
. Эти данные в дальнейшем будут использоваться внешним приложением для прохождения flow авторизации в Time.
Поддерживаемые сценарии авторизации в Time
Authorization Code
В этом сценарии авторизация состоит из следующих шагов:
- Внешнее приложение перенаправляет пользователя, который хочет дать доступ к Time, на страницу авторизации Time
/oauth/authorize
, указав следующие query параметры в URL:response_type=code
- указывает на то, что используется Authorization Code flow и приложение ожидает получить код авторизацииclient_id
- идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Timeredirect_uri
- адрес, на который будет перенаправлен пользователь после авторизации в Time. Должен совпадать с адресом, указанным при регистрации приложения в Time
- Пользователь авторизуется в Time и подтверждает предоставление доступа приложению. После чего пользователь перенаправляется на указанный в
redirect_uri
адрес приложения с query параметромcode
. - Клиентское приложение отправляет
POST
запрос на сервер Time/oauth/access_token
, указав в теле запроса следующие параметры:grant_type=authorization_code
- указывает на то, что приложение ожидает получить токен доступа по коду авторизацииclient_id
- идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Timeclient_secret
- секретный ключ клиентского приложения, который был получен на шаге 4 при регистрации приложения в Timecode
- код авторизации, полученный на предыдущем шаге В ответе на запрос клиентское приложение получит json модель с refresh и access токенами, который можно будет использовать для доступа к Time.
- Для получения нового access токена с помощью refresh токена, клиентское приложение отправляет
POST
запрос на сервер Time/oauth/access_token
, указав в теле запроса следующие параметры:grant_type=refresh_token
- указывает на то, что приложение ожидает получить новый токен доступа по refresh токенуclient_id
- идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Timeclient_secret
- секретный ключ клиентского приложения, который был получен на шаге 4 при регистрации приложения в Timerefresh_token
- refresh токен
В ответе на запрос клиентское приложение получит json модель с новым access токеном.
Implicit Grant
В этом сценарии для авторизации выполняется следующий алгоритм:
- Внешнее приложение направляет пользователя на страницу авторизации Time
/oauth/authorize
, указав следующие query параметры в URL:response_type=token
- указывает на то, используется Implicit Grant flow и приложение ожидает в ответ получить токен доступаclient_id
- идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Timeredirect_uri
- адрес, на который будет перенаправлен пользователь после авторизации в Time. Должен совпадать с адресом, указанным при регистрации приложения в Time
- Пользователь авторизуется в Time и подтверждает предоставление доступа приложению. После чего пользователь перенаправляется на указанный в
redirect_uri
адрес, содержащим access токен в виде URL fragment в параметреaccess_token
.
Список авторизованных OAuth приложений пользователя
В Time доступна возможность просмотра списка авторизованных OAuth приложений. Для этого необходимо:
- Нажать на иконку пользователя в правом верхнем углу и выбрать пункт
Профиль
- Перейти на раздел
Безопасность
и нажать на кнопкуИзменить
в разделеOAuth 2.0 приложения
. - В открывшемся окне будет отображен список OAuth приложений. В этом же окне можно отозвать доступ у приложения, нажав на кнопку
Деавторизация
.
Пример приложения на Go
Для демонстрации работы OAuth2 Service Provider в Time приведен пример приложения на Go, которое позволяет авторизоваться в Time и получить информацию о текущем пользователе. Для работы OAuth2 используется пакет golang.org/x/oauth2, в котором реализованы все необходимые методы для работы с OAuth2.
Для запуска приложения необходимо указать следующие параметры командной строки:
--client-id
- OAuth2 Client ID--client-secret
- OAuth2 Client Secret--host
- хост для запуска приложения, по умолчаниюlocalhost
--port
- порт для запуска приложения, по умолчанию8080
--time-url
- базовый URL мессенджера Time, по умолчаниюhttp://localhost:8065
Если time запущен локально, то можно запустить приложение следующим образом:
go run main.go --client-id <client_id> --client-secret <client_secret>
где <client_id>
и <client_secret>
- значения, полученные при регистрации приложения в Time.
Пример на Go
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"text/template"
"github.com/google/uuid"
"golang.org/x/oauth2"
)
var (
host string
port string
timeURL string
clientID string
clientSecret string
oauthTimeConfig *oauth2.Config
)
func init() {
// Флаги командной строки
flag.StringVar(&host, "host", "localhost", "Хост для запуска приложения")
flag.StringVar(&port, "port", "8080", "Порт для запуска приложения")
flag.StringVar(&timeURL, "time-url", "http://localhost:8065", "Базовый URL мессенджера Time")
flag.StringVar(&clientID, "client-id", "", "OAuth2 Client ID")
flag.StringVar(&clientSecret, "client-secret", "", "OAuth2 Client Secret")
flag.Parse()
oauthTimeConfig = &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: fmt.Sprintf("http://%s:%s/callback", host, port),
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/oauth/authorize", timeURL),
TokenURL: fmt.Sprintf("%s/oauth/access_token", timeURL),
},
}
}
// Хранение токена сессии (для простоты — в памяти)
var tokenStore map[string]*oauth2.Token = make(map[string]*oauth2.Token)
func main() {
if clientID == "" || clientSecret == "" {
fmt.Println("Необходимо указать --client-id и --client-secret")
return
}
http.HandleFunc("/", indexHandler)
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/callback", callbackHandler)
addr := net.JoinHostPort(host, port)
fmt.Printf("Server is running http://%s\n", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("sessionID")
if err != nil {
indexHTML.Execute(w, nil)
return
}
sessionID := cookie.Value
token, ok := tokenStore[sessionID]
if !ok {
indexHTML.Execute(w, nil)
return
}
// Получаем информацию о пользователе
client := oauthTimeConfig.Client(r.Context(), token)
resp, err := client.Get(fmt.Sprintf("%s/api/v4/users/me", timeURL))
if err != nil {
http.Error(w, "Failed to get user info", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
type UserInfo struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
var userInfo UserInfo
userInfoByte, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(userInfoByte, &userInfo); err != nil {
http.Error(w, fmt.Sprintf("Failed to parse user info: %s", err), http.StatusInternalServerError)
return
}
indexHTML.Execute(w, userInfo)
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
url := oauthTimeConfig.AuthCodeURL("")
http.Redirect(w, r, url, http.StatusFound)
}
func callbackHandler(w http.ResponseWriter, r *http.Request) {
// Проверка кода и получение токена
code := r.FormValue("code")
token, err := oauthTimeConfig.Exchange(r.Context(), code)
if err != nil {
http.Error(w, "Failed to exchange token", http.StatusInternalServerError)
return
}
// Сохраняем токен в хранилище
sessionID := uuid.New().String()
tokenStore[sessionID] = token
// Устанавливаем сессию и перенаправляем на главную страницу
http.SetCookie(w, &http.Cookie{
Name: "sessionID",
Value: sessionID,
})
http.Redirect(w, r, "/", http.StatusFound)
}
var indexHTML = template.Must(template.New("index").Parse(`
<!DOCTYPE html>
<html>
<head>
<title>OAuth2 Demo with Time</title>
</head>
<body>
<h1>OAuth2 Demo with Time</h1>
{{ if . }}
<p>Информация о пользователе:</p>
<pre>ID: {{ .ID }}</pre>
<pre>Username: {{ .Username }}</pre>
<pre>Email: {{ .Email }}</pre>
{{ else }}
<a href="/login">Войти через Time</a>
{{ end }}
</body>
</html>
`))