Skip to content

Commit

Permalink
Merge pull request #240 from ww24/add-authorization
Browse files Browse the repository at this point in the history
Add authorization
  • Loading branch information
ww24 authored Apr 18, 2022
2 parents 51a88ac + 745d6f1 commit efb7f21
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 27 deletions.
9 changes: 8 additions & 1 deletion cmd/linebot/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 30 additions & 24 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ var Set = wire.NewSet(

// Config implements repository.Config.
type Config struct {
lineChannelSecret string
lineChannelToken string
conversationIDs *ConversationIDs
addr string
cloudTasksLocation string
cloudTasksQueue string
serviceEndpoint *url.URL
weatherAPI string
weatherAPITimeout time.Duration
browserTimeout time.Duration
imageBucket string
defaultTimezone string
lineChannelSecret string
lineChannelToken string
conversationIDs *ConversationIDs
addr string
cloudTasksLocation string
cloudTasksQueue string
serviceEndpoint *url.URL
weatherAPI string
weatherAPITimeout time.Duration
browserTimeout time.Duration
imageBucket string
defaultTimezone string
invokerServiceAccountID string
}

func NewConfig() (*Config, error) {
Expand Down Expand Up @@ -88,18 +89,19 @@ func NewConfig() (*Config, error) {
}

return &Config{
lineChannelSecret: os.Getenv("LINE_CHANNEL_SECRET"),
lineChannelToken: os.Getenv("LINE_CHANNEL_ACCESS_TOKEN"),
conversationIDs: conversationIDs,
addr: addr,
cloudTasksLocation: os.Getenv("CLOUD_TASKS_LOCATION"),
cloudTasksQueue: os.Getenv("CLOUD_TASKS_QUEUE"),
serviceEndpoint: serviceEndpoint,
weatherAPI: os.Getenv("WEATHER_API"),
weatherAPITimeout: weatherAPITimeout,
browserTimeout: browserTimeout,
imageBucket: os.Getenv("IMAGE_BUCKET"),
defaultTimezone: os.Getenv("DEFAULT_TIMEZONE"),
lineChannelSecret: os.Getenv("LINE_CHANNEL_SECRET"),
lineChannelToken: os.Getenv("LINE_CHANNEL_ACCESS_TOKEN"),
conversationIDs: conversationIDs,
addr: addr,
cloudTasksLocation: os.Getenv("CLOUD_TASKS_LOCATION"),
cloudTasksQueue: os.Getenv("CLOUD_TASKS_QUEUE"),
serviceEndpoint: serviceEndpoint,
weatherAPI: os.Getenv("WEATHER_API"),
weatherAPITimeout: weatherAPITimeout,
browserTimeout: browserTimeout,
imageBucket: os.Getenv("IMAGE_BUCKET"),
defaultTimezone: os.Getenv("DEFAULT_TIMEZONE"),
invokerServiceAccountID: os.Getenv("INVOKER_SERVICE_ACCOUNT_ID"),
}, nil
}

Expand Down Expand Up @@ -184,3 +186,7 @@ func (c *Config) DefaultLocation() *time.Location {
}
return loc
}

func (c *Config) InvokerServiceAccountID() string {
return c.invokerServiceAccountID
}
1 change: 1 addition & 0 deletions domain/repository/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Config interface {
BrowserTimeout() time.Duration
ImageBucket() string
DefaultLocation() *time.Location
InvokerServiceAccountID() string
}

type ConversationIDs interface {
Expand Down
14 changes: 14 additions & 0 deletions mock/mock_repository/mock_config.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 67 additions & 0 deletions presentation/http/authorizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package http

import (
"context"
"net/http"
"strings"

"go.uber.org/zap"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"

"github.com/ww24/linebot/domain/repository"
"github.com/ww24/linebot/logger"
)

type Authorizer struct {
audience string
invokerServiceAccountID string
}

func NewAuthorizer(ctx context.Context, conf repository.Config) (*Authorizer, error) {
serviceEndpoint, err := conf.ServiceEndpoint("")
if err != nil {
return nil, xerrors.New("failed to get service endpoint")
}

return &Authorizer{
audience: serviceEndpoint.String(),
invokerServiceAccountID: conf.InvokerServiceAccountID(),
}, nil
}

func (a *Authorizer) Authorize(ctx context.Context, r *http.Request) error {
validator, err := idtoken.NewValidator(ctx)
if err != nil {
return xerrors.Errorf("failed to create validator to authorize: %w", err)
}

authorization := r.Header.Get("authorization")
if authorization == "" {
return xerrors.New("authorization header is empty")
}

authorization = strings.TrimSpace(authorization)
const prefix = "Bearer "
if !strings.HasPrefix(authorization, prefix) {
return xerrors.New("authorization header is invalid")
}

token := strings.TrimPrefix(authorization, prefix)
payload, err := validator.Validate(ctx, token, a.audience)
if err != nil {
return xerrors.Errorf("failed to validate token: %w", err)
}

if payload.Issuer != "https://accounts.google.com" ||
payload.Subject != a.invokerServiceAccountID {
dl := logger.DefaultLogger(ctx)
dl.Warn("invalid token payload",
zap.String("iss", payload.Issuer),
zap.String("subject", payload.Subject),
)
return xerrors.New("token is not issued by invoker service account")
}

return nil
}
14 changes: 12 additions & 2 deletions presentation/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import (
var Set = wire.NewSet(
NewHandler,
wire.Bind(new(http.Handler), new(*Handler)),
NewAuthorizer,
)

type Handler struct {
log *logger.Logger
bot service.Bot
auth *Authorizer
eventHandler usecase.EventHandler
imageHandler usecase.ImageHandler
middlewares []func(http.Handler) http.Handler
Expand All @@ -36,19 +38,21 @@ type Handler struct {
func NewHandler(
log *logger.Logger,
bot service.Bot,
auth *Authorizer,
eventHandler usecase.EventHandler,
imageHandler usecase.ImageHandler,
) *Handler {
) (*Handler, error) {
return &Handler{
log: log,
bot: bot,
auth: auth,
eventHandler: eventHandler,
imageHandler: imageHandler,
middlewares: []func(http.Handler) http.Handler{
XCTCOpenTelemetry(),
PanicHandler(log),
},
}
}, nil
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -117,6 +121,12 @@ func (h *Handler) executeScheduler() func(w http.ResponseWriter, r *http.Request
return
}

if err := h.auth.Authorize(ctx, r); err != nil {
cl.Info("failed to authorize", zap.Error(err))
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}

if err := h.eventHandler.HandleSchedule(ctx); err != nil {
cl.Error("failed to execute scheduler", zap.Error(err))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
Expand Down
5 changes: 5 additions & 0 deletions terraform/cloud_run_linebot.tf
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ resource "google_cloud_run_service" "linebot" {
name = "IMAGE_BUCKET"
value = google_storage_bucket.image.name
}

env {
name = "INVOKER_SERVICE_ACCOUNT_ID"
value = google_service_account.invoker.unique_id
}
}
}

Expand Down

0 comments on commit efb7f21

Please sign in to comment.