lib/dam/token_flow.go (626 lines of code) (raw):
// Copyright 2019 Google LLC
//
// Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dam
import (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"cloud.google.com/go/logging" /* copybara-comment */
"google.golang.org/grpc/codes" /* copybara-comment */
"google.golang.org/grpc/status" /* copybara-comment */
"golang.org/x/oauth2" /* copybara-comment */
"github.com/golang/protobuf/jsonpb" /* copybara-comment */
"github.com/pborman/uuid" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/adapter" /* copybara-comment: adapter */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/auditlog" /* copybara-comment: auditlog */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/auth" /* copybara-comment: auth */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/errutil" /* copybara-comment: errutil */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/ga4gh" /* copybara-comment: ga4gh */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/httputils" /* copybara-comment: httputils */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/hydra" /* copybara-comment: hydra */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/scim" /* copybara-comment: scim */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/timeutil" /* copybara-comment: timeutil */
pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/dam/v1" /* copybara-comment: go_proto */
)
const (
maxResourceStateSeconds = 300
accountNameLength = 30
)
var (
// resourcePathRE is for realm name, resource name, view name, role name, and interface name lookup only.
resourcePathRE = regexp.MustCompile(`^([^\s/]*)/resources/([^\s/]+)/views/([^\s/]+)/roles/([^\s/]+)/interfaces/([^\s/]+)$`)
// TODO: remove this older path when DDAP no longer uses it
oldResourcePathRE = regexp.MustCompile(`^([^\s/]*)/resources/([^\s/]+)/views/([^\s/]+)/roles/([^\s/]+)$`)
)
func extractBearerToken(r *http.Request) (string, error) {
auth := r.Header.Get("Authorization")
if len(auth) == 0 {
return "", fmt.Errorf("bearer token not found")
}
parts := strings.Split(auth, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return "", fmt.Errorf("token is not a bearer token")
}
return parts[1], nil
}
func parseTTL(maxAgeStr, ttlStr string) (time.Duration, error) {
if len(maxAgeStr) > 0 {
return timeutil.ParseSeconds(maxAgeStr)
}
if len(ttlStr) == 0 {
return defaultTTL, nil
}
ttl, err := timeutil.ParseDuration(ttlStr)
if err != nil {
return 0, fmt.Errorf("TTL parameter %q format error: %v", ttlStr, err)
}
return ttl, nil
}
func extractTTL(maxAgeStr, ttlStr string) (time.Duration, error) {
// TODO ttl params should remove.
ttl, err := parseTTL(maxAgeStr, ttlStr)
if err != nil {
return 0, err
}
if ttl == 0 {
return defaultTTL, nil
}
if ttl < 0 || ttl > maxTTL {
return 0, fmt.Errorf("TTL parameter %q out of range: must be positive and not exceed %s", ttlStr, maxTTLStr)
}
return ttl, nil
}
func responseKeyFile(r *http.Request) bool {
return httputils.QueryParam(r, "response_type") == "key-file-type"
}
func (s *Service) generateResourceToken(ctx context.Context, clientID, resourceName, viewName, role, iface string, ttl time.Duration, useKeyFile bool, id *ga4gh.Identity, cfg *pb.DamConfig, res *pb.Resource, view *pb.View) (*pb.ResourceResults_ResourceAccess, int, error) {
sRole, err := adapter.ResolveServiceRole(role, view, res, cfg)
if err != nil {
return nil, http.StatusInternalServerError, err
}
if !viewHasRole(view, role) {
return nil, http.StatusBadRequest, fmt.Errorf("role %q is not defined on resource %q view %q", role, resourceName, viewName)
}
st, ok := cfg.ServiceTemplates[view.ServiceTemplate]
if !ok {
return nil, http.StatusInternalServerError, fmt.Errorf("view %q service template %q is not defined", viewName, view.ServiceTemplate)
}
adapt := s.adapters.ByServiceName[st.ServiceName]
var aggregates []*adapter.AggregateView
if adapt.IsAggregator() {
aggregates, err = resolveAggregates(res, view, cfg, s.adapters)
if err != nil {
return nil, http.StatusInternalServerError, err
}
}
tokenFormat := ""
if useKeyFile {
tokenFormat = "application/json"
}
adapterAction := &adapter.Action{
Aggregates: aggregates,
Identity: id,
Issuer: s.gatekeeperTokenIssuerURL(),
ClientID: clientID,
Config: cfg,
GrantRole: role,
MaxTTL: maxTTL,
ResourceID: resourceName,
Resource: res,
ServiceRole: sRole,
ServiceTemplate: st,
TTL: ttl,
ViewID: viewName,
View: view,
Interface: iface,
TokenFormat: tokenFormat,
}
result, err := adapt.MintToken(ctx, adapterAction)
if err != nil {
return nil, http.StatusServiceUnavailable, err
}
return &pb.ResourceResults_ResourceAccess{
Credentials: result.Credentials,
Labels: result.Labels,
ExpiresIn: uint32(ttl.Seconds()),
}, http.StatusOK, nil
}
func (s *Service) oauthConf(brokerName string, broker *pb.TrustedIssuer, clientSecret string, scopes []string) *oauth2.Config {
return &oauth2.Config{
ClientID: broker.ClientId,
ClientSecret: clientSecret,
Scopes: scopes,
Endpoint: oauth2.Endpoint{
AuthURL: broker.AuthUrl,
TokenURL: broker.TokenUrl,
},
RedirectURL: s.domainURL + loggedInPath,
}
}
func (s *Service) resourceViewRoleFromRequest(list []string) ([]resourceViewRole, error) {
out := []resourceViewRole{}
if len(list) == 0 {
return nil, fmt.Errorf("resource parameter not found")
}
for _, res := range list {
if !strings.HasPrefix(res, s.domainURL) {
return nil, fmt.Errorf("requested resource %q not in this DAM", res)
}
prefix := s.domainURL + "/dam/"
path := strings.ReplaceAll(res, prefix, "")
m := resourcePathRE.FindStringSubmatch(path)
if len(m) == 0 {
// TODO: remove support for oldResourcePath
m = oldResourcePathRE.FindStringSubmatch(path)
if len(m) > 4 {
m = append(m, "")
}
}
if len(m) > 5 {
out = append(out, resourceViewRole{realm: m[1], resource: m[2], view: m[3], role: m[4], interf: m[5], url: res})
continue
}
return nil, fmt.Errorf("resource %q has invalid format", res)
}
return out, nil
}
type resourceViewRole struct {
realm string
resource string
view string
role string
interf string
url string
}
type authHandlerIn struct {
tokenType pb.ResourceTokenRequestState_TokenType
realm string
stateID string
redirect string
ttl time.Duration
clientID string
clientName string
responseKeyFile bool
resources []resourceViewRole
challenge string
requestedAudience []string
requestedScope []string
}
type authHandlerOut struct {
oauth *oauth2.Config
stateID string
}
func (s *Service) auth(ctx context.Context, in authHandlerIn) (_ *authHandlerOut, ferr error) {
tx, err := s.store.Tx(true)
if err != nil {
return nil, status.Errorf(codes.Unavailable, err.Error())
}
defer func() {
err := tx.Finish()
if ferr == nil && err != nil {
ferr = status.Errorf(codes.Unavailable, err.Error())
}
}()
sec, err := s.loadSecrets(tx)
if err != nil {
return nil, status.Errorf(codes.Unavailable, err.Error())
}
realm := in.realm
if in.tokenType == pb.ResourceTokenRequestState_DATASET {
if len(in.resources) == 0 {
return nil, status.Errorf(codes.FailedPrecondition, "empty resource list")
}
realm = in.resources[0].realm
}
cfg, err := s.loadConfig(tx, realm)
if err != nil {
return nil, status.Errorf(codes.Unavailable, err.Error())
}
broker, ok := cfg.TrustedIssuers[s.defaultBroker]
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "broker %q is not defined", s.defaultBroker)
}
clientSecret, ok := sec.GetBrokerSecrets()[broker.ClientId]
if !ok {
return nil, status.Errorf(codes.FailedPrecondition, "client secret of broker %q is not defined", s.defaultBroker)
}
var list []*pb.ResourceTokenRequestState_Resource
for _, rvr := range in.resources {
if rvr.realm != realm {
return nil, status.Errorf(codes.Aborted, "cannot authorize resources using different realms")
}
resName := rvr.resource
viewName := rvr.view
roleName := rvr.role
interf := rvr.interf
if err := checkName(resName); err != nil {
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
if err := checkName(viewName); err != nil {
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
res, ok := cfg.Resources[resName]
if !ok {
return nil, status.Errorf(codes.NotFound, "resource not found: %q", resName)
}
view, ok := res.Views[viewName]
if !ok {
return nil, status.Errorf(codes.NotFound, "view %q not found for resource %q", viewName, resName)
}
grantRole := roleName
if len(grantRole) == 0 {
grantRole = view.DefaultRole
}
if !viewHasRole(view, grantRole) {
return nil, status.Errorf(codes.FailedPrecondition, "role %q is not defined on resource %q view %q", grantRole, resName, viewName)
}
st, ok := cfg.ServiceTemplates[view.ServiceTemplate]
if !ok {
return nil, status.Errorf(codes.Internal, "service template %q is invalid for resource %q view %q", view.ServiceTemplate, resName, viewName)
}
// TODO: remove support for oldResourcePath
if len(interf) == 0 {
for k := range st.Interfaces {
interf = k
break
}
}
if _, ok = st.Interfaces[interf]; !ok {
return nil, status.Errorf(codes.FailedPrecondition, "interface %q is not defined on resource %q view %q service template %q", interf, resName, viewName, view.ServiceTemplate)
}
list = append(list, &pb.ResourceTokenRequestState_Resource{
Realm: rvr.realm,
Resource: resName,
View: viewName,
Role: grantRole,
Interface: interf,
Url: rvr.url,
})
}
// TODO: need support real policy filter
scopes := []string{"openid", "ga4gh_passport_v1", "identities", "account_admin"}
if in.tokenType == pb.ResourceTokenRequestState_ENDPOINT {
scopes = []string{"openid", "identities"}
}
sID := uuid.New()
state := &pb.ResourceTokenRequestState{
Type: in.tokenType,
ClientId: in.clientID,
ClientName: in.clientName,
State: in.stateID,
Broker: s.defaultBroker,
Redirect: in.redirect,
Ttl: int64(in.ttl),
ResponseKeyFile: in.responseKeyFile,
Resources: list,
LoginChallenge: in.challenge,
EpochSeconds: time.Now().Unix(),
Realm: realm,
RequestedAudience: in.requestedAudience,
RequestedScope: in.requestedScope,
}
err = s.store.WriteTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, sID, storage.LatestRev, state, nil, tx)
if err != nil {
return nil, status.Errorf(codes.Unavailable, err.Error())
}
conf := s.oauthConf(s.defaultBroker, broker, clientSecret, scopes)
return &authHandlerOut{
oauth: conf,
stateID: sID,
}, nil
}
type loggedInHandlerIn struct {
authCode string
stateID string
errStr string
errDesc string
r *http.Request
}
type loggedInHandlerOut struct {
redirect string
stateID string
subject string
identities []string
}
func (s *Service) loggedIn(ctx context.Context, in loggedInHandlerIn) (_ *loggedInHandlerOut, _ string, ferr error) {
tx, err := s.store.Tx(true)
if err != nil {
return nil, "", status.Errorf(codes.Unavailable, err.Error())
}
defer func() {
err := tx.Finish()
if ferr == nil && err != nil {
ferr = status.Errorf(codes.Internal, err.Error())
}
}()
state := &pb.ResourceTokenRequestState{}
err = s.store.ReadTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, in.stateID, storage.LatestRev, state, tx)
if err != nil {
if storage.ErrNotFound(err) {
return nil, "", status.Errorf(codes.InvalidArgument, err.Error())
}
return nil, "", status.Errorf(codes.Unavailable, err.Error())
}
if len(in.errStr) > 0 || len(in.errDesc) > 0 {
return nil, state.LoginChallenge, errutil.WithErrorReason(in.errStr, status.Errorf(codes.Unauthenticated, in.errDesc))
}
if len(in.authCode) == 0 {
return nil, state.LoginChallenge, status.Errorf(codes.InvalidArgument, "no auth code")
}
sec, err := s.loadSecrets(tx)
if err != nil {
return nil, state.LoginChallenge, status.Errorf(codes.Unavailable, err.Error())
}
realm := state.Realm
cfg, err := s.loadConfig(tx, realm)
if err != nil {
return nil, state.LoginChallenge, status.Errorf(codes.Unavailable, err.Error())
}
broker, ok := cfg.TrustedIssuers[state.Broker]
if !ok {
return nil, state.LoginChallenge, status.Errorf(codes.InvalidArgument, "unknown identity broker %q", state.Broker)
}
clientSecret, ok := sec.GetBrokerSecrets()[broker.ClientId]
if !ok {
return nil, state.LoginChallenge, status.Errorf(codes.FailedPrecondition, "client secret of broker %q is not defined", s.defaultBroker)
}
conf := s.oauthConf(state.Broker, broker, clientSecret, []string{})
tok, err := conf.Exchange(ctx, in.authCode)
if err != nil {
return nil, state.LoginChallenge, status.Errorf(codes.Unauthenticated, "token exchange failed. %s", err)
}
id, err := s.upstreamTokenToPassportIdentity(ctx, cfg, tx, tok.AccessToken, broker.ClientId)
if err != nil {
return nil, state.LoginChallenge, status.Errorf(codes.Unauthenticated, err.Error())
}
var subject string
if subject, err = s.createOrUpdateAccount(ctx, in.r, id, state.Broker, state.Realm, tx); err != nil {
return nil, state.LoginChallenge, err
}
// Add the upstream id to identities, and change id subject to dam account subject.
id.Identities[id.Subject] = nil
id.Subject = subject
if state.Type == pb.ResourceTokenRequestState_DATASET {
out, err := s.loggedInForDatasetToken(ctx, id, state, cfg, in.stateID, realm, tx)
return out, state.LoginChallenge, err
}
out, err := s.loggedInForEndpointToken(id, state, in.stateID, tx)
return out, state.LoginChallenge, err
}
func (s *Service) createOrUpdateAccount(ctx context.Context, r *http.Request, id *ga4gh.Identity, broker, realm string, tx storage.Tx) (string, error) {
lookup, err := s.scim.LoadAccountLookup(realm, id.Subject, tx)
if err != nil {
return "", status.Errorf(codes.Unavailable, "%v", err)
}
// TODO: move state checks to storage package.
if lookup != nil && lookup.State == storage.StateActive {
subject := lookup.Subject
acct, _, err := s.scim.LoadAccount(subject, realm, true, tx)
if err != nil {
return "", status.Errorf(codes.Unavailable, "%v", err)
}
if acct.State == storage.StateDisabled {
// Reject using a DISABLED account.
return "", status.Errorf(codes.PermissionDenied, "this account has been disabled, please contact the system administrator")
}
acct, err = scim.UpdateIdentityInAccount(ctx, id, broker, acct, s.encryption)
if err != nil {
return "", err
}
if err := s.scim.SaveAccount(nil, acct, "REFRESH claims "+id.Subject, id.Subject, realm, r, tx); err != nil {
return "", status.Errorf(codes.Unavailable, "%v", err)
}
return subject, nil
}
// Create an account for the identity automatically.
genlen := accountNameLength
accountPrefix := "dam_"
acct, lookup, err := scim.NewAccount(ctx, s.encryption, id, broker, accountPrefix, genlen)
if err != nil {
return "", err
}
if err := s.scim.SaveAccount(nil, acct, "Create Account", id.Subject, realm, r, tx); err != nil {
return "", fmt.Errorf("service dependencies not available; try again later")
}
if err := s.scim.SaveAccountLookup(lookup, realm, id.Subject, r, id, tx); err != nil {
return "", fmt.Errorf("service dependencies not available; try again later")
}
return acct.Properties.Subject, nil
}
func resourceToString(res *pb.ResourceTokenRequestState_Resource) string {
return fmt.Sprintf("%s/%s/%s/%s", res.Realm, res.Resource, res.View, res.Role)
}
func writePolicyDeccisionLog(logger *logging.Client, id *ga4gh.Identity, res *pb.ResourceTokenRequestState_Resource, ttl time.Duration, cartID string, cfgRevision int64, err error) {
log := &auditlog.PolicyDecisionLog{
TokenID: id.ID,
TokenSubject: id.Subject,
TokenIssuer: id.Issuer,
Resource: resourceToString(res),
TTL: timeutil.TTLString(ttl),
PassAuthCheck: true,
CartID: cartID,
ConfigRevision: cfgRevision,
}
if err != nil {
log.PassAuthCheck = false
log.ErrorType = errutil.ErrorReason(err)
if reject := rejectedPolicy(err); reject != nil {
marshaler := jsonpb.Marshaler{}
s, err := marshaler.MarshalToString(reject)
if err != nil {
s = err.Error()
}
log.Message = s
} else {
log.Message = err.Error()
}
}
auditlog.WritePolicyDecisionLog(logger, log)
}
func (s *Service) loggedInForDatasetToken(ctx context.Context, id *ga4gh.Identity, state *pb.ResourceTokenRequestState, cfg *pb.DamConfig, stateID, realm string, tx storage.Tx) (*loggedInHandlerOut, error) {
ttl := time.Duration(state.Ttl)
list := state.Resources
if len(list) == 0 {
return nil, status.Errorf(codes.Internal, "empty resource list")
}
for _, r := range list {
if r.Realm != realm {
return nil, status.Errorf(codes.Aborted, "cannot authorize resources using different realms")
}
err := checkAuthorization(ctx, id, ttl, r.Resource, r.View, r.Role, cfg, state.ClientId, s.ValidateCfgOpts(realm, tx))
writePolicyDeccisionLog(s.logger, id, r, ttl, stateID, cfg.Revision, err)
if err != nil {
return nil, err
}
}
state.Issuer = id.Issuer
state.Subject = id.Subject
err := s.store.WriteTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, state, nil, tx)
if err != nil {
return nil, status.Errorf(codes.Unavailable, err.Error())
}
return &loggedInHandlerOut{
stateID: stateID,
subject: id.Subject,
}, nil
}
func (s *Service) loggedInForEndpointToken(id *ga4gh.Identity, state *pb.ResourceTokenRequestState, stateID string, tx storage.Tx) (*loggedInHandlerOut, error) {
identities := []string{id.Subject}
for k := range id.Identities {
identities = append(identities, k)
}
state.Identities = identities
state.Issuer = id.Issuer
state.Subject = id.Subject
err := s.store.WriteTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, state, nil, tx)
if err != nil {
return nil, status.Errorf(codes.Unavailable, err.Error())
}
return &loggedInHandlerOut{
subject: id.Subject,
stateID: stateID,
}, nil
}
// ResourceTokens returns a set of access tokens for a set of resources.
func (s *Service) ResourceTokens(w http.ResponseWriter, r *http.Request) {
resp, err := s.fetchResourceTokens(r)
if err != nil {
httputils.WriteError(w, err)
return
}
httputils.WriteResp(w, resp)
}
func (s *Service) fetchResourceTokens(r *http.Request) (_ *pb.ResourceResults, ferr error) {
tx, err := s.store.Tx(false)
if err != nil {
return nil, status.Errorf(codes.Unavailable, "%v", err)
}
defer func() {
err := tx.Finish()
if ferr == nil {
ferr = err
}
}()
a, err := auth.FromContext(r.Context())
if err != nil {
return nil, err
}
cart := ""
if s.useHydra {
cart, err = s.extractCartFromAccessToken(a.ID)
if err != nil {
return nil, err
}
} else {
return nil, status.Errorf(codes.Unimplemented, "Unimplemented oidc provider")
}
state, id, err := s.resourceTokenState(cart, tx)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%v", err)
}
if len(state.Resources) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "empty resource list")
}
cfg, err := s.loadConfig(tx, state.Resources[0].Realm)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%v", err)
}
ctx := r.Context()
keyFile := false
out := &pb.ResourceResults{
Resources: make(map[string]*pb.ResourceResults_ResourceDescriptor),
Access: make(map[string]*pb.ResourceResults_ResourceAccess),
EpochSeconds: uint32(time.Now().Unix()),
}
for i, r := range state.Resources {
res, ok := cfg.Resources[r.Resource]
if !ok {
return nil, status.Errorf(codes.NotFound, "resource not found: %q", r.Resource)
}
view, ok := res.Views[r.View]
if !ok {
return nil, status.Errorf(codes.NotFound, "view %q not found for resource %q", r.View, r.Resource)
}
result, st, err := s.generateResourceToken(ctx, state.ClientId, r.Resource, r.View, r.Role, r.Interface, time.Duration(state.Ttl), keyFile, id, cfg, res, view)
if err != nil {
return nil, status.Errorf(httputils.RPCCode(st), "%v", err)
}
access := strconv.Itoa(i)
interMap := map[string]*pb.ResourceResults_InterfaceEntry{}
for k, v := range makeViewInterfaces(view, res, cfg, s.adapters) {
entry := &pb.ResourceResults_InterfaceEntry{}
interMap[k] = entry
for _, uri := range v.Uri {
entry.Items = append(entry.Items, &pb.ResourceResults_ResourceInterface{Uri: uri, Labels: v.Labels})
}
}
out.Resources[r.Url] = &pb.ResourceResults_ResourceDescriptor{
Interfaces: interMap,
Permissions: makeRoleCategories(view, r.Role, cfg),
Access: access,
}
out.Access[access] = &pb.ResourceResults_ResourceAccess{
Credentials: result.Credentials,
Labels: result.Labels,
}
}
return out, nil
}
func (s *Service) resourceTokenState(stateID string, tx storage.Tx) (*pb.ResourceTokenRequestState, *ga4gh.Identity, error) {
state := &pb.ResourceTokenRequestState{}
err := s.store.ReadTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, state, tx)
if err != nil {
return nil, nil, err
}
if len(state.Issuer) == 0 || len(state.Subject) == 0 {
return nil, nil, fmt.Errorf("unauthorized")
}
now := time.Now().Unix()
if now-state.EpochSeconds > maxResourceStateSeconds {
return nil, nil, fmt.Errorf("authorization expired")
}
return state, &ga4gh.Identity{
Issuer: state.Issuer,
Subject: state.Subject,
}, nil
}
// LoggedInHandler implements endpoint "/loggedin" for broker auth code redirect.
func (s *Service) LoggedInHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
stateID := httputils.QueryParam(r, "state")
if len(stateID) == 0 {
httputils.WriteError(w, status.Errorf(codes.InvalidArgument, "request must include state"))
}
code := httputils.QueryParam(r, "code")
errStr := httputils.QueryParam(r, "error")
errDesc := httputils.QueryParam(r, "error_description")
out, challenge, err := s.loggedIn(r.Context(), loggedInHandlerIn{authCode: code, stateID: stateID, errStr: errStr, errDesc: errDesc, r: r})
if err != nil {
if s.useHydra && len(challenge) > 0 {
hydra.SendLoginReject(w, r, s.httpClient, s.hydraAdminURL, challenge, err)
} else {
httputils.WriteError(w, err)
}
return
}
if s.useHydra {
ext := map[string]interface{}{}
if len(out.identities) > 0 {
ext["identities"] = out.identities
}
hydra.SendLoginSuccess(w, r, s.httpClient, s.hydraAdminURL, challenge, out.subject, out.stateID, ext)
return
}
httputils.WriteError(w, status.Errorf(codes.Unimplemented, "oidc service not supported"))
}