Skip to content

Commit

Permalink
Add the login package (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
francescomari authored Nov 4, 2019
1 parent 1f39dcd commit 5e54733
Show file tree
Hide file tree
Showing 11 changed files with 1,081 additions and 0 deletions.
71 changes: 71 additions & 0 deletions login/callback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2019 Adobe. All rights reserved.
// This file is licensed to you under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may obtain a copy
// of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
// OF ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.

package login

import (
"fmt"
"net/http"

"github.com/adobe/ims-go/ims"
)

type callbackBackend interface {
Token(r *ims.TokenRequest) (*ims.TokenResponse, error)
}

type callbackMiddleware struct {
client callbackBackend
state string
clientID string
clientSecret string
scope []string
next http.Handler
}

func (h *callbackMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query()

uerr := values.Get("error")
if uerr != "" {
serveError(h.next, w, r, fmt.Errorf("backend error: %s", uerr))
return
}

state := values.Get("state")
if state == "" {
serveError(h.next, w, r, fmt.Errorf("missing state parameter"))
return
}

if h.state != state {
serveError(h.next, w, r, fmt.Errorf("invalid state parameter"))
return
}

code := values.Get("code")
if code == "" {
serveError(h.next, w, r, fmt.Errorf("missing code parameter"))
return
}

res, err := h.client.Token(&ims.TokenRequest{
Code: code,
ClientID: h.clientID,
ClientSecret: h.clientSecret,
Scope: h.scope,
})
if err != nil {
serveError(h.next, w, r, fmt.Errorf("obtaining access token: %v", err))
return
}

serveResult(h.next, w, r, res)
}
202 changes: 202 additions & 0 deletions login/callback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright 2019 Adobe. All rights reserved.
// This file is licensed to you under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may obtain a copy
// of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
// OF ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.

package login

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/adobe/ims-go/ims"
)

type testCallbackBackend func(r *ims.TokenRequest) (*ims.TokenResponse, error)

func (b testCallbackBackend) Token(r *ims.TokenRequest) (*ims.TokenResponse, error) {
return b(r)
}

func urlWithParams(path string, params map[string]string) string {
values := url.Values{}

for k, v := range params {
values.Set(k, v)
}

u := url.URL{
Path: path,
RawQuery: values.Encode(),
}

return u.String()
}

func TestCallback(t *testing.T) {
m := &callbackMiddleware{
state: "state",
clientID: "client-id",
clientSecret: "client-secret",
scope: []string{"a", "b"},

client: testCallbackBackend(func(r *ims.TokenRequest) (*ims.TokenResponse, error) {
if r.Code != "code" {
t.Fatalf("invalid code: %v", r.Code)
}
if r.ClientID != "client-id" {
t.Fatalf("invalid client ID: %v", r.ClientID)
}
if r.ClientSecret != "client-secret" {
t.Fatalf("invalid client secret: %v", r.ClientSecret)
}
if len(r.Scope) != 2 && r.Scope[0] != "a" && r.Scope[1] != "b" {
t.Fatalf("invalid scope: %v", r.Scope)
}
return &ims.TokenResponse{}, nil
}),

next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
res, ok := r.Context().Value(contextKeyResult).(*ims.TokenResponse)
if !ok {
t.Fatalf("invalid context value")
}
if res == nil {
t.Fatalf("no response returned")
}
}),
}

target := urlWithParams("/", map[string]string{
"code": "code",
"state": "state",
})

m.ServeHTTP(nil, httptest.NewRequest(http.MethodGet, target, nil))
}

func TestCallbackError(t *testing.T) {
m := &callbackMiddleware{
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err, ok := r.Context().Value(contextKeyError).(error)
if !ok {
t.Fatalf("invalid context value")
}
if err == nil {
t.Fatalf("no error returned")
}
if err.Error() != "backend error: error" {
t.Fatalf("invalid error: %v", err)
}
}),
}

target := urlWithParams("/", map[string]string{
"error": "error",
})

m.ServeHTTP(nil, httptest.NewRequest(http.MethodGet, target, nil))
}

func TestCallbackNoState(t *testing.T) {
m := &callbackMiddleware{
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err, ok := r.Context().Value(contextKeyError).(error)
if !ok {
t.Fatalf("invalid context value")
}
if err == nil {
t.Fatalf("no error returned")
}
if err.Error() != "missing state parameter" {
t.Fatalf("invalid error: %v", err)
}
}),
}

m.ServeHTTP(nil, httptest.NewRequest(http.MethodGet, "/", nil))
}

func TestCallbackInvalidState(t *testing.T) {
m := &callbackMiddleware{
state: "state",
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err, ok := r.Context().Value(contextKeyError).(error)
if !ok {
t.Fatalf("invalid context value")
}
if err == nil {
t.Fatalf("no error returned")
}
if err.Error() != "invalid state parameter" {
t.Fatalf("invalid error: %v", err)
}
}),
}

target := urlWithParams("/", map[string]string{
"state": "invalid-state",
})

m.ServeHTTP(nil, httptest.NewRequest(http.MethodGet, target, nil))
}

func TestCallbackMissingCode(t *testing.T) {
m := &callbackMiddleware{
state: "state",
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err, ok := r.Context().Value(contextKeyError).(error)
if !ok {
t.Fatalf("invalid context value")
}
if err == nil {
t.Fatalf("no error returned")
}
if err.Error() != "missing code parameter" {
t.Fatalf("invalid error: %v", err)
}
}),
}

target := urlWithParams("/", map[string]string{
"state": "state",
})

m.ServeHTTP(nil, httptest.NewRequest(http.MethodGet, target, nil))
}

func TestCallbackBackendError(t *testing.T) {
m := &callbackMiddleware{
state: "state",
client: testCallbackBackend(func(r *ims.TokenRequest) (*ims.TokenResponse, error) {
return nil, fmt.Errorf("error")
}),
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err, ok := r.Context().Value(contextKeyError).(error)
if !ok {
t.Fatalf("invalid context value")
}
if err == nil {
t.Fatalf("no error returned")
}
if err.Error() != "obtaining access token: error" {
t.Fatalf("invalid error: %v", err)
}
}),
}

target := urlWithParams("/", map[string]string{
"state": "state",
"code": "code",
})

m.ServeHTTP(nil, httptest.NewRequest(http.MethodGet, target, nil))
}
37 changes: 37 additions & 0 deletions login/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// ADOBE CONFIDENTIAL
// ___________________
//
// Copyright 2019 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE: All information contained herein is, and remains the property of
// Adobe Systems Incorporated and its suppliers, if any. The intellectual and
// technical concepts contained herein are proprietary to Adobe Systems
// Incorporated and its suppliers and are protected by trade secret or copyright
// law. Dissemination of this information or reproduction of this material is
// strictly forbidden unless prior written permission is obtained from Adobe
// Systems Incorporated.

package login

import (
"context"
"net/http"

"github.com/adobe/ims-go/ims"
)

type contextKey string

var (
contextKeyError = contextKey("error")
contextKeyResult = contextKey("result")
)

func serveError(h http.Handler, w http.ResponseWriter, r *http.Request, err error) {
h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKeyError, err)))
}

func serveResult(h http.Handler, w http.ResponseWriter, r *http.Request, res *ims.TokenResponse) {
h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKeyResult, res)))
}
44 changes: 44 additions & 0 deletions login/redirect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2019 Adobe. All rights reserved.
// This file is licensed to you under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may obtain a copy
// of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
// OF ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.

package login

import (
"fmt"
"net/http"

"github.com/adobe/ims-go/ims"
)

type redirectBackend interface {
AuthorizeURL(cfg *ims.AuthorizeURLConfig) (string, error)
}

type redirectMiddleware struct {
client redirectBackend
clientID string
scope []string
state string
next http.Handler
}

func (h *redirectMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
url, err := h.client.AuthorizeURL(&ims.AuthorizeURLConfig{
ClientID: h.clientID,
Scope: h.scope,
State: h.state,
})
if err != nil {
serveError(h.next, w, r, fmt.Errorf("generate authorization URL: %v", err))
return
}

http.Redirect(w, r, url, http.StatusFound)
}
Loading

0 comments on commit 5e54733

Please sign in to comment.