Перейти к основному содержимому

OAuth2 Service Provider

Time может выступать в качестве поставщика услуг OAuth2, что позволяет внешним приложениям получать доступ к ресурсам Time от имени пользователей.

Регистрация OAuth приложения в Time

Чтобы настроить Time в качестве поставщика услуг OAuth2, необходимо выполнить следующие шаги:

  1. От системного администратора в системной консоли включить опцию "Включить поставщика услуг OAuth 2.0" в разделе Настройки -> Интеграции.
  2. В разделе Интеграции -> OAuth 2.0 приложения нажав на кнопку "Подключить OAuth 2.0 приложение" зарегестрировать новое OAuth приложение.
    • В поле Домашняя страница указать адрес приложения.
    • В поле URL обратного вызова указать адрес, на который будет перенаправлен пользователь после авторизации в Time.
  3. В открывшемся окне заполнить информацию о приложении и нажать кнопку Сохранить.
  4. После сохранения будут предоставлены Client ID и Client Secret. Эти данные в дальнейшем будут использоваться внешним приложением для прохождения flow авторизации в Time.

Поддерживаемые сценарии авторизации в Time

Authorization Code

В этом сценарии авторизация состоит из следующих шагов:

  1. Внешнее приложение перенаправляет пользователя, который хочет дать доступ к Time, на страницу авторизации Time /oauth/authorize, указав следующие query параметры в URL:
    • response_type=code - указывает на то, что используется Authorization Code flow и приложение ожидает получить код авторизации
    • client_id - идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Time
    • redirect_uri - адрес, на который будет перенаправлен пользователь после авторизации в Time. Должен совпадать с адресом, указанным при регистрации приложения в Time
  2. Пользователь авторизуется в Time и подтверждает предоставление доступа приложению. После чего пользователь перенаправляется на указанный в redirect_uri адрес приложения с query параметром code.
  3. Клиентское приложение отправляет POST запрос на сервер Time /oauth/access_token, указав в теле запроса следующие параметры:
    • grant_type=authorization_code - указывает на то, что приложение ожидает получить токен доступа по коду авторизации
    • client_id - идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Time
    • client_secret - секретный ключ клиентского приложения, который был получен на шаге 4 при регистрации приложения в Time
    • code - код авторизации, полученный на предыдущем шаге В ответе на запрос клиентское приложение получит json модель с refresh и access токенами, который можно будет использовать для доступа к Time.
  4. Для получения нового access токена с помощью refresh токена, клиентское приложение отправляет POST запрос на сервер Time /oauth/access_token, указав в теле запроса следующие параметры:
    • grant_type=refresh_token - указывает на то, что приложение ожидает получить новый токен доступа по refresh токену
    • client_id - идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Time
    • client_secret - секретный ключ клиентского приложения, который был получен на шаге 4 при регистрации приложения в Time
    • refresh_token - refresh токен

В ответе на запрос клиентское приложение получит json модель с новым access токеном.

Implicit Grant

В этом сценарии для авторизации выполняется следующий алгоритм:

  1. Внешнее приложение направляет пользователя на страницу авторизации Time /oauth/authorize, указав следующие query параметры в URL:
    • response_type=token - указывает на то, используется Implicit Grant flow и приложение ожидает в ответ получить токен доступа
    • client_id - идентификатор клиентского приложения, который был получен на шаге 4 при регистрации приложения в Time
    • redirect_uri - адрес, на который будет перенаправлен пользователь после авторизации в Time. Должен совпадать с адресом, указанным при регистрации приложения в Time
  2. Пользователь авторизуется в Time и подтверждает предоставление доступа приложению. После чего пользователь перенаправляется на указанный в redirect_uri адрес, содержащим access токен в виде URL fragment в параметре access_token.

Список авторизованных OAuth приложений пользователя

В Time доступна возможность просмотра списка авторизованных OAuth приложений. Для этого необходимо:

  1. Нажать на иконку пользователя в правом верхнем углу и выбрать пункт Профиль
  2. Перейти на раздел Безопасность и нажать на кнопку Изменить в разделе OAuth 2.0 приложения.
  3. В открывшемся окне будет отображен список 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>
`))