Skip to content
This repository was archived by the owner on Mar 11, 2021. It is now read-only.

Commit

Permalink
REST API for account linking (#136)
Browse files Browse the repository at this point in the history
The new API for account linking which doesn't use keycloak IDPs linking.

## Linking Accounts

```
POST /api/token/link
Content-Type: application/x-www-form-urlencoded
Payload: for=<resource>&token=<access_token>&scope=<scope>&redirect=<redirect_url>
```

**`<resource>`** - Resource we need to link accounts for. For example https://github.com/somecoolrepo or https://console.starter-us-east-2.openshift.com/console/project/coolproject
**`<scope>`** - required scope. Multiple scopes can be specified by separating them with a space. Optional. If not defined then the default scope is used. Not supported in the first version!
**`<redirect>`** - after successful linking the client will be redirected to this URL. If not specified then the URL from the “Referer” header will be used. If both “Referer” header and “redirect” param are missing then a Bar Request response will be returned.
**`<access_token>`** - user’s access token

If the token for such user already exists then the account will be re-linked and the token will be updated.
In the first version, we support only **github** and **openshift** represents OSO-us-starter-2. Later an individual **openshift** provider will be associated with the user during signup by the registration app, as part of multicluster support.

**Example:**
```
POST /api/token/link
Content-Type: application/x-www-form-urlencoded
for=https://github.com/somecoolrepo&token=ABSDEF12345678990&redirect=https%3A%2F%2Fopenshift.io&scope=user%20public_repo
```

## Re-linking Accounts

If some services catches 401 when trying to use Git or OS token this service should return 401 to UI with the following header:

`WWW-Authenticate: Link url=<link_url_optional>, description=”<description_optional>”`

UI should initiate re-linking when it catches such 401 response.

Fixes #134
  • Loading branch information
alexeykazakov authored and sbose78 committed Oct 13, 2017
1 parent 69a0229 commit 693b405
Show file tree
Hide file tree
Showing 26 changed files with 1,060 additions and 234 deletions.
66 changes: 66 additions & 0 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ const (
varServiceAccountPrivateKeyIDDeprecated = "serviceaccount.privatekeyid.deprecated"
varServiceAccountPrivateKey = "serviceaccount.privatekey"
varServiceAccountPrivateKeyID = "serviceaccount.privatekeyid"
varGitHubClientID = "github.client.id"
varGitHubClientSecret = "github.client.secret"
varGitHubClientDefaultScopes = "github.client.defaultscopes"
varOSOClientApiUrl = "oso.client.apiurl"
varOSOClientID = "oso.client.id"
varOSOClientSecret = "oso.client.secret"
varOSOClientDefaultScopes = "oso.client.defaultscopes"
varOSOLinkingEnabled = "oso.linking.enabled"
varTLSInsecureSkipVerify = "tls.insecureskipverify"
varNotApprovedRedirect = "notapproved.redirect"
varHeaderMaxLength = "header.maxlength"
varCacheControlUsers = "cachecontrol.users"
Expand Down Expand Up @@ -178,6 +187,14 @@ func (c *ConfigurationData) setConfigDefaults() {
c.v.SetDefault(varKeycloakDomainPrefix, defaultKeycloakDomainPrefix)
c.v.SetDefault(varKeycloakTesUserName, defaultKeycloakTesUserName)
c.v.SetDefault(varKeycloakTesUserSecret, defaultKeycloakTesUserSecret)
c.v.SetDefault(varGitHubClientID, "c6a3a6280e9650ba27d8")
c.v.SetDefault(varGitHubClientSecret, "48d1498c849616dfecf83cf74f22dfb361ee2511")
c.v.SetDefault(varGitHubClientDefaultScopes, "admin:repo_hook read:org repo user gist")
c.v.SetDefault(varOSOClientApiUrl, "https://api.starter-us-east-2.openshift.com")
c.v.SetDefault(varOSOClientID, "oso-id")
c.v.SetDefault(varOSOClientSecret, "oso-secret")
c.v.SetDefault(varOSOClientDefaultScopes, "user:full")
c.v.SetDefault(varTLSInsecureSkipVerify, false) // Do not set to true in production! True can be used only for testing.

// Max number of users returned when searching users
c.v.SetDefault(varUsersListLimit, 50)
Expand Down Expand Up @@ -317,6 +334,55 @@ func (c *ConfigurationData) GetServiceAccountPrivateKey() ([]byte, string) {
return []byte(c.v.GetString(varServiceAccountPrivateKey)), c.v.GetString(varServiceAccountPrivateKeyID)
}

// GetGitHubClientID return GitHub client ID used to link GitHub accounts
func (c *ConfigurationData) GetGitHubClientID() string {
return c.v.GetString(varGitHubClientID)
}

// GetGitHubClientSecret return GitHub client secret used to link GitHub accounts
func (c *ConfigurationData) GetGitHubClientSecret() string {
return c.v.GetString(varGitHubClientSecret)
}

// GetGitHubClientDefaultScopes return default scopes used to link GitHub accounts
func (c *ConfigurationData) GetGitHubClientDefaultScopes() string {
return c.v.GetString(varGitHubClientDefaultScopes)
}

// GetOpenShiftClientApiUrl return OpenShift client API URL used to link OpenShift accounts
func (c *ConfigurationData) GetOpenShiftClientApiUrl() string {
return c.v.GetString(varOSOClientApiUrl)
}

// GetOpenShiftClientID return OpenShift client ID used to link OpenShift accounts
func (c *ConfigurationData) GetOpenShiftClientID() string {
return c.v.GetString(varOSOClientID)
}

// GetGitHubClientSecret return OpenShift client secret used to link OpenShift accounts
func (c *ConfigurationData) GetOpenShiftClientSecret() string {
return c.v.GetString(varOSOClientSecret)
}

// GetOpenShiftClientDefaultScopes return default scopes used to link OpenShift accounts
func (c *ConfigurationData) GetOpenShiftClientDefaultScopes() string {
return c.v.GetString(varOSOClientDefaultScopes)
}

// IsOpenShiftLinkingEnabled returns true if OpenShift account linking is enabled
func (c *ConfigurationData) IsOpenShiftLinkingEnabled() bool {
if c.v.IsSet(varOSOLinkingEnabled) {
return c.v.GetBool(varOSOLinkingEnabled)
}
return !c.IsPostgresDeveloperModeEnabled()
}

// IsTLSInsecureSkipVerify returns true the client should not verify the
// server's certificate chain and host name. This mode should be used only for testing.
func (c *ConfigurationData) IsTLSInsecureSkipVerify() bool {
return c.v.GetBool(varTLSInsecureSkipVerify)
}

// GetNotApprovedRedirect returns the URL to redirect to if the user is not approved
// May return empty string which means an unauthorized error should be returned instead of redirecting the user
func (c *ConfigurationData) GetNotApprovedRedirect() string {
Expand Down
5 changes: 5 additions & 0 deletions configuration/configuration_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ func TestGetMaxHeaderSizeSetByEnvVaribaleOK(t *testing.T) {
assert.Equal(t, envValue, viperValue)
}

func TestIsTLSInsecureSkipVerifySetToFalse(t *testing.T) {
resource.Require(t, resource.UnitTest)
require.False(t, config.IsTLSInsecureSkipVerify())
}

func generateEnvKey(yamlKey string) string {
return "AUTH_" + strings.ToUpper(strings.Replace(yamlKey, ".", "_", -1))
}
Expand Down
2 changes: 2 additions & 0 deletions controller/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type LoginConfiguration interface {
GetKeycloakClientID() string
GetKeycloakSecret() string
IsPostgresDeveloperModeEnabled() bool
IsOpenShiftLinkingEnabled() bool
GetOpenShiftClientApiUrl() string
GetKeycloakTestUserName() string
GetKeycloakTestUserSecret() string
GetKeycloakTestUser2Name() string
Expand Down
6 changes: 3 additions & 3 deletions controller/login_blackbox_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package controller_test

import (
"testing"

"context"
"testing"

"github.com/fabric8-services/fabric8-auth/account"
"github.com/fabric8-services/fabric8-auth/app"
Expand All @@ -17,6 +16,7 @@ import (
"github.com/fabric8-services/fabric8-auth/resource"
testsupport "github.com/fabric8-services/fabric8-auth/test"
testtoken "github.com/fabric8-services/fabric8-auth/test/token"
"github.com/fabric8-services/fabric8-auth/token/oauth"

"github.com/goadesign/goa"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -82,7 +82,7 @@ func (rest *TestLoginREST) TestOfflineAccessOK() {

type TestLoginService struct{}

func (t TestLoginService) Perform(ctx *app.LoginLoginContext, config login.OauthConfig, serviceConfig login.LoginServiceConfiguration) error {
func (t TestLoginService) Perform(ctx *app.LoginLoginContext, config oauth.OauthConfig, serviceConfig login.LoginServiceConfiguration) error {
return ctx.TemporaryRedirect()
}

Expand Down
61 changes: 55 additions & 6 deletions controller/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package controller

import (
"context"
"time"

"net/http"
"net/url"
"time"

"github.com/fabric8-services/fabric8-auth/account"
"github.com/fabric8-services/fabric8-auth/app"
Expand All @@ -16,24 +15,27 @@ import (
"github.com/fabric8-services/fabric8-auth/rest"
"github.com/fabric8-services/fabric8-auth/test"
"github.com/fabric8-services/fabric8-auth/token"
"github.com/fabric8-services/fabric8-auth/token/link"

"strings"

"github.com/goadesign/goa"
errs "github.com/pkg/errors"
)

const maxRecentSpacesForRPT = 10

// TokenController implements the login resource.
type TokenController struct {
*goa.Controller
Auth login.KeycloakOAuthService
LinkService link.LinkOAuthService
TokenManager token.Manager
Configuration LoginConfiguration
identityRepository account.IdentityRepository
}

// NewTokenController creates a token controller.
func NewTokenController(service *goa.Service, auth *login.KeycloakOAuthProvider, tokenManager token.Manager, configuration LoginConfiguration, identityRepository account.IdentityRepository) *TokenController {
return &TokenController{Controller: service.NewController("token"), Auth: auth, TokenManager: tokenManager, Configuration: configuration, identityRepository: identityRepository}
func NewTokenController(service *goa.Service, auth *login.KeycloakOAuthProvider, linkService link.LinkOAuthService, tokenManager token.Manager, configuration LoginConfiguration, identityRepository account.IdentityRepository) *TokenController {
return &TokenController{Controller: service.NewController("token"), Auth: auth, LinkService: linkService, TokenManager: tokenManager, Configuration: configuration, identityRepository: identityRepository}
}

// Keys returns public keys which should be used to verify tokens
Expand Down Expand Up @@ -189,3 +191,50 @@ func GenerateUserToken(ctx context.Context, tokenEndpoint string, configuration

return convertToken(*t), nil
}

// Link links the user account to an external resource provider such as GitHub
func (c *TokenController) Link(ctx *app.LinkTokenContext) error {
tokenClaims, err := c.TokenManager.ParseToken(ctx, ctx.Payload.Token)
if err != nil {
log.Error(ctx, map[string]interface{}{
"err": err,
"token": ctx.Payload.Token,
}, "unable to parse token")
return jsonapi.JSONErrorResponse(ctx, errors.NewUnauthorizedError(err.Error()))
}
identityID := tokenClaims.StandardClaims.Subject

var redirectURL string
if ctx.Payload.Redirect == nil {
redirectURL = ctx.RequestData.Header.Get("Referer")
if redirectURL == "" {
return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterError("redirect", "empty").Expected("redirect param or Referer header should be specified"))
}
} else {
redirectURL = *ctx.Payload.Redirect
}

if !c.Configuration.IsOpenShiftLinkingEnabled() && strings.HasPrefix(ctx.Payload.For, c.Configuration.GetOpenShiftClientApiUrl()) {
// OSO account linking is disabled by default in Dev Mode.
ctx.ResponseData.Header().Set("Location", redirectURL)
return ctx.SeeOther()
}

redirectLocation, err := c.LinkService.ProviderLocation(ctx, ctx.RequestData, identityID, ctx.Payload.For, redirectURL)
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}

ctx.ResponseData.Header().Set("Location", redirectLocation)
return ctx.SeeOther()
}

// Callback is called by an external oauth2 resource provider such as GitHub as part of user's account linking flow
func (c *TokenController) Callback(ctx *app.CallbackTokenContext) error {
redirectLocation, err := c.LinkService.Callback(ctx, ctx.RequestData, ctx.State, ctx.Code)
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}
ctx.ResponseData.Header().Set("Location", redirectLocation)
return ctx.TemporaryRedirect()
}
Loading

0 comments on commit 693b405

Please sign in to comment.