This is an example of an HTTP app (source), written in Go and runnable on http://localhost:8082
.
manifest.json
, declares itself an HTTP application, requests permissions, and binds itself to locations in the Mattermost user interface.bindings
function it declares three commands: configure
, connect
, and send
.send
function mentions the user by their Google name, and lists their Google Calendars.To install “Hello, OAuth2” on a locally-running instance of Mattermost follow these steps (go 1.16 is required):
Make sure you have followed the Quick Start Guide prerequisite steps.
git clone https://github.com/mattermost/mattermost-plugin-apps.git
cd mattermost-plugin-apps/examples/go/hello-oauth2
go run
Run the following Mattermost slash command:
/apps install http http://localhost:8082/manifest.json
You need to configure your Google API Credentials for the app. Use $MATTERMOST_SITE_URL$/com.mattermost.apps/apps/hello-oauth2/oauth2/remote/complete
for the Authorized redirect URIs
field. After configuring the credentials, in the Mattermost Desktop app, run:
/hello-oauth2 configure --client-id $CLIENT_ID --client-secret $CLIENT_SECRET
Now, you can connect your account to Google with /hello-oauth2 connect
command, and then try /hello-oauth2 send
.
Hello OAuth2! is an HTTP app, it requests the permissions to act as a System Admin to change the app’s OAuth2 config, as a user to connect and send. It binds itself to /
commands.
{
"app_id": "hello-oauth2",
"version": "0.8.0",
"display_name": "Hello, OAuth2!",
"app_type": "http",
"icon": "icon.png",
"homepage_url": "https://github.com/mattermost/mattermost-plugin-apps/examples/go/hello-oauth2",
"requested_permissions": [
"act_as_user",
"remote_oauth2"
],
"requested_locations": [
"/command"
],
"http": {
"root_url": "http://localhost:8082"
}
}
The Hello OAuth2 app creates three commands: /hello-oauth2 configure | connect | send
.
{
"type": "ok",
"data": [
{
"location": "/command",
"bindings": [
{
"icon": "icon.png",
"label": "helloworld",
"description": "Hello remote (third-party) OAuth2 App",
"hint": "[configure | connect | send]",
"bindings": [
{
"location": "configure",
"label": "configure",
"call": {
"path": "/configure"
}
},
{
"location": "connect",
"label": "connect",
"call": {
"path": "/connect"
}
},
{
"location": "send",
"label": "send",
"call": {
"path": "/send"
}
}
]
}
]
}
]
}
/hello-oauth2 configure
sets up the Google OAuth2 credentials. It accepts two string flags, --client-id
and --client-secret
. Submit will require a user access token to effect the changes.
{
"type": "form",
"form": {
"title": "Configures Google OAuth2 App credentials",
"icon": "icon.png",
"fields": [
{
"type": "text",
"name": "client_id",
"label": "client-id",
"is_required": true
},
{
"type": "text",
"name": "client_secret",
"label": "client-secret",
"is_required": true
}
],
"call": {
"path": "/configure",
"expand": {
"acting_user_access_token": "all"
}
}
}
}
The command handler uses an admin-only StoreOAuth2App
API to store the credentials, and make them available to future calls with expand.oauth2_app="all"
.
func configure(w http.ResponseWriter, req *http.Request) {
creq := apps.CallRequest{}
json.NewDecoder(req.Body).Decode(&creq)
clientID, _ := creq.Values["client_id"].(string)
clientSecret, _ := creq.Values["client_secret"].(string)
asUser := appclient.AsActingUser(creq.Context)
asUser.StoreOAuth2App(creq.Context.AppID, clientID, clientSecret)
json.NewEncoder(w).Encode(apps.CallResponse{
Markdown: "updated OAuth client credentials",
})
}
connect
command /hello-oauth2 connect
formats and displays a link that starts the OAuth2 flow with the remote system. The URL (provided to the app in the Context
) is handled by the apps framework. It will:
oauth2Connect
to generate the remote URL that starts the flow.Note expand.oauth2_app="all"
in the form definition, it includes the app’s OAuth2 Mattermost-hosted callback URL in the request context. This command should soon be provided by the framework, see MM-34561.
{
"type": "form",
"form": {
"title": "Connect to Google",
"icon": "icon.png",
"call": {
"path": "/connect",
"expand": {
"oauth2_app": "all"
}
}
}
}
func connect(w http.ResponseWriter, req *http.Request) {
creq := apps.CallRequest{}
json.NewDecoder(req.Body).Decode(&creq)
json.NewEncoder(w).Encode(apps.CallResponse{
Markdown: md.Markdownf("[Connect](%s) to Google.", creq.Context.OAuth2.ConnectURL),
})
}
To handle the OAuth2 connect
flow, the app provides two calls: /oauth2/connect
that returns the URL to redirect the user to, and /oauth2/complete
which gets invoked once the flow is finished, and the state
parameter is verified.
// Handle an OAuth2 connect URL request.
http.HandleFunc("/oauth2/connect", oauth2Connect)
// Handle a successful OAuth2 connection.
http.HandleFunc("/oauth2/complete", oauth2Complete)
oauth2Connect
extracts the necessary data from the request’s context and values (“state”), and composes a Google OAuth2 initial URL.
func oauth2Connect(w http.ResponseWriter, req *http.Request) {
creq := apps.CallRequest{}
json.NewDecoder(req.Body).Decode(&creq)
state, _ := creq.Values["state"].(string)
url := oauth2Config(&creq).AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
json.NewEncoder(w).Encode(apps.CallResponse{
Type: apps.CallResponseTypeOK,
Data: url,
})
}
oauth2Complete
is called upon the successful completion (including the validation of the “state”). It is responsible for creating an OAuth2 token, and storing it in the Mattermost OAuth2 user store.
func oauth2Complete(w http.ResponseWriter, req *http.Request) {
creq := apps.CallRequest{}
json.NewDecoder(req.Body).Decode(&creq)
code, _ := creq.Values["code"].(string)
token, _ := oauth2Config(&creq).Exchange(context.Background(), code)
asActingUser := appclient.AsActingUser(creq.Context)
asActingUser.StoreOAuth2User(creq.Context.AppID, token)
json.NewEncoder(w).Encode(apps.CallResponse{})
}
The app is responsible for composing its own remote OAuth2 config, using the remote system-specific settings. The ClientID
and ClientSecret
are stored in Mattermost OAuth2App record, and are included in the request context if specified with expand.oauth2_app="all"
.
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
func oauth2Config(creq *apps.CallRequest) *oauth2.Config {
return &oauth2.Config{
ClientID: creq.Context.OAuth2.ClientID,
ClientSecret: creq.Context.OAuth2.ClientSecret,
Endpoint: google.Endpoint,
RedirectURL: creq.Context.OAuth2.CompleteURL,
Scopes: []string{
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
},
}
}
send
command /hello-oauth2 send
sends the user a message that includes the Google user name on the account, and lists the Google Calendars. The form requests that submit calls expand oauth2_user
which is where the app stored the OAuth2 token upon a successful connect.
{
"type": "form",
"form": {
"title": "Send a Google-connected 'hello, world!' message",
"icon": "icon.png",
"call": {
"path": "/send",
"expand": {
"oauth2_user": "all"
}
}
}
}
func send(w http.ResponseWriter, req *http.Request) {
creq := apps.CallRequest{}
json.NewDecoder(req.Body).Decode(&creq)
oauthConfig := oauth2Config(&creq)
token := oauth2.Token{}
remarshal(&token, creq.Context.OAuth2.User) // go JSON is quirky!
ctx := context.Background()
tokenSource := oauthConfig.TokenSource(ctx, &token)
oauth2Service, _ := oauth2api.NewService(ctx, option.WithTokenSource(tokenSource))
calService, _ := calendar.NewService(ctx, option.WithTokenSource(tokenSource))
uiService := oauth2api.NewUserinfoService(oauth2Service)
ui, _ := uiService.V2.Me.Get().Do()
message := fmt.Sprintf("Hello from Google, [%s](mailto:%s)!", ui.Name, ui.Email)
cl, _ := calService.CalendarList.List().Do()
if cl != nil && len(cl.Items) > 0 {
message += " You have the following calendars:\n"
for _, item := range cl.Items {
message += "- " + item.Summary + "\n"
}
} else {
message += " You have no calendars.\n"
}
json.NewEncoder(w).Encode(apps.CallResponse{
Markdown: md.MD(message),
})
}