From 2841853d37aa02128d493bafbf39b520d133bb31 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 6 Jun 2022 22:08:32 +0530 Subject: [PATCH] feat: add linkedin login --- app/package-lock.json | 30 ++-- app/package.json | 2 +- .../components/EnvComponents/OAuthConfig.tsx | 40 ++++- dashboard/src/constants.ts | 4 + dashboard/src/graphql/queries/index.ts | 3 +- dashboard/src/pages/Environment.tsx | 3 + server/constants/env.go | 4 + server/constants/oauth_info_urls.go | 3 + server/constants/signup_methods.go | 2 + server/env/env.go | 16 ++ server/graph/generated/generated.go | 153 ++++++++++++++++++ server/graph/model/models_gen.go | 5 + server/graph/schema.graphqls | 5 + server/handlers/oauth_callback.go | 93 ++++++++++- server/handlers/oauth_login.go | 17 ++ server/oauth/oauth.go | 20 +++ server/resolvers/env.go | 6 + server/resolvers/meta.go | 13 ++ 18 files changed, 400 insertions(+), 19 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 79b6010..1546545 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^0.17.0", + "@authorizerdev/authorizer-react": "^0.23.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -26,9 +26,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.10.0.tgz", - "integrity": "sha512-REM8FLD/Ej9gzA2zDGDAke6QFss33ubePlTDmLDmIYUuQmpHFlO5mCCS6nVsKkN7F/Bcwkmp+eUNQjkdGCaKLg==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.12.0.tgz", + "integrity": "sha512-XgRxAkpRobbp15DeHygfOebCxlPJAXbVaLDckYyuz/PUDTyeMIG65RV5rQHYcL4oeoPqNc42dewwM3ST8JSiNg==", "dependencies": { "node-fetch": "^2.6.1" }, @@ -37,11 +37,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.17.0.tgz", - "integrity": "sha512-7WcNCU7hDFkVfFb8LcJXFwWiLYd8aY78z1AbNPxCa2Cw5G85PaRkzjKybP6h01ITVOHO6M03lLwPj8p6Sr6fEg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.23.0.tgz", + "integrity": "sha512-vOwwrrAorxhVsqpf3BO2In8PMg8RAbGBFu8uLDOvUzkwG0ny5CPg6jLx9+dCkRRsqgB+agBoQoIuXEUP0ijsTA==", "dependencies": { - "@authorizerdev/authorizer-js": "^0.10.0", + "@authorizerdev/authorizer-js": "^0.12.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -852,19 +852,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.10.0.tgz", - "integrity": "sha512-REM8FLD/Ej9gzA2zDGDAke6QFss33ubePlTDmLDmIYUuQmpHFlO5mCCS6nVsKkN7F/Bcwkmp+eUNQjkdGCaKLg==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.12.0.tgz", + "integrity": "sha512-XgRxAkpRobbp15DeHygfOebCxlPJAXbVaLDckYyuz/PUDTyeMIG65RV5rQHYcL4oeoPqNc42dewwM3ST8JSiNg==", "requires": { "node-fetch": "^2.6.1" } }, "@authorizerdev/authorizer-react": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.17.0.tgz", - "integrity": "sha512-7WcNCU7hDFkVfFb8LcJXFwWiLYd8aY78z1AbNPxCa2Cw5G85PaRkzjKybP6h01ITVOHO6M03lLwPj8p6Sr6fEg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.23.0.tgz", + "integrity": "sha512-vOwwrrAorxhVsqpf3BO2In8PMg8RAbGBFu8uLDOvUzkwG0ny5CPg6jLx9+dCkRRsqgB+agBoQoIuXEUP0ijsTA==", "requires": { - "@authorizerdev/authorizer-js": "^0.10.0", + "@authorizerdev/authorizer-js": "^0.12.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index aed0f07..b4f8a59 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^0.17.0", + "@authorizerdev/authorizer-react": "^0.23.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/dashboard/src/components/EnvComponents/OAuthConfig.tsx b/dashboard/src/components/EnvComponents/OAuthConfig.tsx index e2c9eeb..5a24982 100644 --- a/dashboard/src/components/EnvComponents/OAuthConfig.tsx +++ b/dashboard/src/components/EnvComponents/OAuthConfig.tsx @@ -9,7 +9,7 @@ import { Divider, useMediaQuery, } from '@chakra-ui/react'; -import { FaGoogle, FaGithub, FaFacebookF } from 'react-icons/fa'; +import { FaGoogle, FaGithub, FaFacebookF, FaLinkedin } from 'react-icons/fa'; import { TextInputType, HiddenInputType } from '../../constants'; const OAuthConfig = ({ @@ -182,6 +182,44 @@ const OAuthConfig = ({ /> + +
+ +
+
+ +
+
+ +
+
diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index e0f9d3d..086bf82 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -7,6 +7,7 @@ export const TextInputType = { GOOGLE_CLIENT_ID: 'GOOGLE_CLIENT_ID', GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID', FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID', + LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID', JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM', REDIS_URL: 'REDIS_URL', SMTP_HOST: 'SMTP_HOST', @@ -31,6 +32,7 @@ export const HiddenInputType = { GOOGLE_CLIENT_SECRET: 'GOOGLE_CLIENT_SECRET', GITHUB_CLIENT_SECRET: 'GITHUB_CLIENT_SECRET', FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET', + LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET', JWT_SECRET: 'JWT_SECRET', SMTP_PASSWORD: 'SMTP_PASSWORD', ADMIN_SECRET: 'ADMIN_SECRET', @@ -99,6 +101,8 @@ export interface envVarTypes { GITHUB_CLIENT_SECRET: string; FACEBOOK_CLIENT_ID: string; FACEBOOK_CLIENT_SECRET: string; + LINKEDIN_CLIENT_ID: string; + LINKEDIN_CLIENT_SECRET: string; ROLES: [string] | []; DEFAULT_ROLES: [string] | []; PROTECTED_ROLES: [string] | []; diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index cd55475..346c3cb 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -26,7 +26,8 @@ export const EnvVariablesQuery = ` GITHUB_CLIENT_SECRET, FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_SECRET, - ROLES, + LINKEDIN_CLIENT_ID, + LINKEDIN_CLIENT_SECRET, DEFAULT_ROLES, PROTECTED_ROLES, JWT_TYPE, diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index 169c62f..a9032cb 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -46,6 +46,8 @@ const Environment = () => { GITHUB_CLIENT_SECRET: '', FACEBOOK_CLIENT_ID: '', FACEBOOK_CLIENT_SECRET: '', + LINKEDIN_CLIENT_ID: '', + LINKEDIN_CLIENT_SECRET: '', ROLES: [], DEFAULT_ROLES: [], PROTECTED_ROLES: [], @@ -83,6 +85,7 @@ const Environment = () => { GOOGLE_CLIENT_SECRET: false, GITHUB_CLIENT_SECRET: false, FACEBOOK_CLIENT_SECRET: false, + LINKEDIN_CLIENT_SECRET: false, JWT_SECRET: false, SMTP_PASSWORD: false, ADMIN_SECRET: false, diff --git a/server/constants/env.go b/server/constants/env.go index e8c88df..c9ab6fb 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -73,6 +73,10 @@ const ( EnvKeyFacebookClientID = "FACEBOOK_CLIENT_ID" // EnvKeyFacebookClientSecret key for env variable FACEBOOK_CLIENT_SECRET EnvKeyFacebookClientSecret = "FACEBOOK_CLIENT_SECRET" + // EnvKeyLinkedinClientID key for env variable LINKEDIN_CLIENT_ID + EnvKeyLinkedInClientID = "LINKEDIN_CLIENT_ID" + // EnvKeyLinkedinClientSecret key for env variable LINKEDIN_CLIENT_SECRET + EnvKeyLinkedInClientSecret = "LINKEDIN_CLIENT_SECRET" // EnvKeyOrganizationName key for env variable ORGANIZATION_NAME EnvKeyOrganizationName = "ORGANIZATION_NAME" // EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO diff --git a/server/constants/oauth_info_urls.go b/server/constants/oauth_info_urls.go index 4944e3d..875add5 100644 --- a/server/constants/oauth_info_urls.go +++ b/server/constants/oauth_info_urls.go @@ -8,4 +8,7 @@ const ( FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token=" // Ref: https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps#3-your-github-app-accesses-the-api-with-the-users-access-token GithubUserInfoURL = "https://api.github.com/user" + // Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api + LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))" + LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" ) diff --git a/server/constants/signup_methods.go b/server/constants/signup_methods.go index 3ca373c..7db56b7 100644 --- a/server/constants/signup_methods.go +++ b/server/constants/signup_methods.go @@ -11,4 +11,6 @@ const ( SignupMethodGithub = "github" // SignupMethodFacebook is the facebook signup method SignupMethodFacebook = "facebook" + // SignupMethodLinkedin is the linkedin signup method + SignupMethodLinkedIn = "linkedin" ) diff --git a/server/env/env.go b/server/env/env.go index e80fbe4..397d052 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -68,6 +68,8 @@ func InitAllEnv() error { osGithubClientSecret := os.Getenv(constants.EnvKeyGithubClientSecret) osFacebookClientID := os.Getenv(constants.EnvKeyFacebookClientID) osFacebookClientSecret := os.Getenv(constants.EnvKeyFacebookClientSecret) + osLinkedInClientID := os.Getenv(constants.EnvKeyLinkedInClientID) + osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret) osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL) osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName) osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo) @@ -345,6 +347,20 @@ func InitAllEnv() error { envData[constants.EnvKeyFacebookClientSecret] = osFacebookClientSecret } + if val, ok := envData[constants.EnvKeyLinkedInClientID]; !ok || val == "" { + envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID + } + if osFacebookClientID != "" && envData[constants.EnvKeyLinkedInClientID] != osFacebookClientID { + envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID + } + + if val, ok := envData[constants.EnvKeyLinkedInClientSecret]; !ok || val == "" { + envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret + } + if osFacebookClientSecret != "" && envData[constants.EnvKeyLinkedInClientSecret] != osFacebookClientSecret { + envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret + } + if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" { envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/") } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 258c6fe..5444d20 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -85,6 +85,8 @@ type ComplexityRoot struct { JwtRoleClaim func(childComplexity int) int JwtSecret func(childComplexity int) int JwtType func(childComplexity int) int + LinkedinClientID func(childComplexity int) int + LinkedinClientSecret func(childComplexity int) int OrganizationLogo func(childComplexity int) int OrganizationName func(childComplexity int) int ProtectedRoles func(childComplexity int) int @@ -116,6 +118,7 @@ type ComplexityRoot struct { IsFacebookLoginEnabled func(childComplexity int) int IsGithubLoginEnabled func(childComplexity int) int IsGoogleLoginEnabled func(childComplexity int) int + IsLinkedinLoginEnabled func(childComplexity int) int IsMagicLinkLoginEnabled func(childComplexity int) int IsSignUpEnabled func(childComplexity int) int Version func(childComplexity int) int @@ -528,6 +531,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Env.JwtType(childComplexity), true + case "Env.LINKEDIN_CLIENT_ID": + if e.complexity.Env.LinkedinClientID == nil { + break + } + + return e.complexity.Env.LinkedinClientID(childComplexity), true + + case "Env.LINKEDIN_CLIENT_SECRET": + if e.complexity.Env.LinkedinClientSecret == nil { + break + } + + return e.complexity.Env.LinkedinClientSecret(childComplexity), true + case "Env.ORGANIZATION_LOGO": if e.complexity.Env.OrganizationLogo == nil { break @@ -682,6 +699,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.IsGoogleLoginEnabled(childComplexity), true + case "Meta.is_linkedin_login_enabled": + if e.complexity.Meta.IsLinkedinLoginEnabled == nil { + break + } + + return e.complexity.Meta.IsLinkedinLoginEnabled(childComplexity), true + case "Meta.is_magic_link_login_enabled": if e.complexity.Meta.IsMagicLinkLoginEnabled == nil { break @@ -1352,6 +1376,7 @@ type Meta { is_google_login_enabled: Boolean! is_facebook_login_enabled: Boolean! is_github_login_enabled: Boolean! + is_linkedin_login_enabled: Boolean! is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! @@ -1462,6 +1487,8 @@ type Env { GITHUB_CLIENT_SECRET: String FACEBOOK_CLIENT_ID: String FACEBOOK_CLIENT_SECRET: String + LINKEDIN_CLIENT_ID: String + LINKEDIN_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } @@ -1509,6 +1536,8 @@ input UpdateEnvInput { GITHUB_CLIENT_SECRET: String FACEBOOK_CLIENT_ID: String FACEBOOK_CLIENT_SECRET: String + LINKEDIN_CLIENT_ID: String + LINKEDIN_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } @@ -3602,6 +3631,70 @@ func (ec *executionContext) _Env_FACEBOOK_CLIENT_SECRET(ctx context.Context, fie return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _Env_LINKEDIN_CLIENT_ID(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.LinkedinClientID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _Env_LINKEDIN_CLIENT_SECRET(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.LinkedinClientSecret, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Env_ORGANIZATION_NAME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4007,6 +4100,41 @@ func (ec *executionContext) _Meta_is_github_login_enabled(ctx context.Context, f return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Meta_is_linkedin_login_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Meta", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsLinkedinLoginEnabled, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Meta_is_email_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -8563,6 +8691,22 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob if err != nil { return it, err } + case "LINKEDIN_CLIENT_ID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("LINKEDIN_CLIENT_ID")) + it.LinkedinClientID, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "LINKEDIN_CLIENT_SECRET": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("LINKEDIN_CLIENT_SECRET")) + it.LinkedinClientSecret, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } case "ORGANIZATION_NAME": var err error @@ -9031,6 +9175,10 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._Env_FACEBOOK_CLIENT_ID(ctx, field, obj) case "FACEBOOK_CLIENT_SECRET": out.Values[i] = ec._Env_FACEBOOK_CLIENT_SECRET(ctx, field, obj) + case "LINKEDIN_CLIENT_ID": + out.Values[i] = ec._Env_LINKEDIN_CLIENT_ID(ctx, field, obj) + case "LINKEDIN_CLIENT_SECRET": + out.Values[i] = ec._Env_LINKEDIN_CLIENT_SECRET(ctx, field, obj) case "ORGANIZATION_NAME": out.Values[i] = ec._Env_ORGANIZATION_NAME(ctx, field, obj) case "ORGANIZATION_LOGO": @@ -9142,6 +9290,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } + case "is_linkedin_login_enabled": + out.Values[i] = ec._Meta_is_linkedin_login_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "is_email_verification_enabled": out.Values[i] = ec._Meta_is_email_verification_enabled(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 2f24f7c..d2fa5d7 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -65,6 +65,8 @@ type Env struct { GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` + LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` + LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"` } @@ -116,6 +118,7 @@ type Meta struct { IsGoogleLoginEnabled bool `json:"is_google_login_enabled"` IsFacebookLoginEnabled bool `json:"is_facebook_login_enabled"` IsGithubLoginEnabled bool `json:"is_github_login_enabled"` + IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"` IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` @@ -216,6 +219,8 @@ type UpdateEnvInput struct { GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` + LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` + LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"` } diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 84797ee..5303b34 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -18,6 +18,7 @@ type Meta { is_google_login_enabled: Boolean! is_facebook_login_enabled: Boolean! is_github_login_enabled: Boolean! + is_linkedin_login_enabled: Boolean! is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! @@ -128,6 +129,8 @@ type Env { GITHUB_CLIENT_SECRET: String FACEBOOK_CLIENT_ID: String FACEBOOK_CLIENT_SECRET: String + LINKEDIN_CLIENT_ID: String + LINKEDIN_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } @@ -175,6 +178,8 @@ input UpdateEnvInput { GITHUB_CLIENT_SECRET: String FACEBOOK_CLIENT_ID: String FACEBOOK_CLIENT_SECRET: String + LINKEDIN_CLIENT_ID: String + LINKEDIN_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 6801f0e..5028c4f 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -60,6 +60,8 @@ func OAuthCallbackHandler() gin.HandlerFunc { user, err = processGithubUserInfo(code) case constants.SignupMethodFacebook: user, err = processFacebookUserInfo(code) + case constants.SignupMethodLinkedIn: + user, err = processLinkedInUserInfo(code) default: log.Info("Invalid oauth provider") err = fmt.Errorf(`invalid oauth provider`) @@ -283,6 +285,10 @@ func processGithubUserInfo(code string) (models.User, error) { log.Debug("Failed to read github user info response body: ", err) return user, fmt.Errorf("failed to read github response body: %s", err.Error()) } + if response.StatusCode >= 400 { + log.Debug("Failed to request linkedin user info: ", string(body)) + return user, fmt.Errorf("failed to request linkedin user info: %s", string(body)) + } userRawData := make(map[string]string) json.Unmarshal(body, &userRawData) @@ -335,7 +341,10 @@ func processFacebookUserInfo(code string) (models.User, error) { log.Debug("Failed to read facebook response: ", err) return user, fmt.Errorf("failed to read facebook response body: %s", err.Error()) } - + if response.StatusCode >= 400 { + log.Debug("Failed to request linkedin user info: ", string(body)) + return user, fmt.Errorf("failed to request linkedin user info: %s", string(body)) + } userRawData := make(map[string]interface{}) json.Unmarshal(body, &userRawData) @@ -356,3 +365,85 @@ func processFacebookUserInfo(code string) (models.User, error) { return user, nil } + +func processLinkedInUserInfo(code string) (models.User, error) { + user := models.User{} + token, err := oauth.OAuthProviders.LinkedInConfig.Exchange(oauth2.NoContext, code) + if err != nil { + log.Debug("Failed to exchange code for token: ", err) + return user, fmt.Errorf("invalid linkedin exchange code: %s", err.Error()) + } + + client := http.Client{} + req, err := http.NewRequest("GET", constants.LinkedInUserInfoURL, nil) + if err != nil { + log.Debug("Failed to create linkedin user info request: ", err) + return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error()) + } + req.Header = http.Header{ + "Authorization": []string{fmt.Sprintf("Bearer %s", token.AccessToken)}, + } + + response, err := client.Do(req) + if err != nil { + log.Debug("Failed to request linkedin user info: ", err) + return user, err + } + + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Debug("Failed to read linkedin user info response body: ", err) + return user, fmt.Errorf("failed to read linkedin response body: %s", err.Error()) + } + + if response.StatusCode >= 400 { + log.Debug("Failed to request linkedin user info: ", string(body)) + return user, fmt.Errorf("failed to request linkedin user info: %s", string(body)) + } + + userRawData := make(map[string]interface{}) + json.Unmarshal(body, &userRawData) + + req, err = http.NewRequest("GET", constants.LinkedInEmailURL, nil) + if err != nil { + log.Debug("Failed to create linkedin email info request: ", err) + return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error()) + } + req.Header = http.Header{ + "Authorization": []string{fmt.Sprintf("Bearer %s", token.AccessToken)}, + } + + response, err = client.Do(req) + if err != nil { + log.Debug("Failed to request linkedin email info: ", err) + return user, err + } + + defer response.Body.Close() + body, err = ioutil.ReadAll(response.Body) + if err != nil { + log.Debug("Failed to read linkedin email info response body: ", err) + return user, fmt.Errorf("failed to read linkedin email response body: %s", err.Error()) + } + if response.StatusCode >= 400 { + log.Debug("Failed to request linkedin user info: ", string(body)) + return user, fmt.Errorf("failed to request linkedin user info: %s", string(body)) + } + emailRawData := make(map[string]interface{}) + json.Unmarshal(body, &emailRawData) + + firstName := userRawData["localizedFirstName"].(string) + lastName := userRawData["localizedLastName"].(string) + profilePicture := userRawData["profilePicture"].(map[string]interface{})["displayImage~"].(map[string]interface{})["elements"].([]interface{})[0].(map[string]interface{})["identifiers"].([]interface{})[0].(map[string]interface{})["identifier"].(string) + emailAddress := emailRawData["elements"].([]interface{})[0].(map[string]interface{})["handle~"].(map[string]interface{})["emailAddress"].(string) + + user = models.User{ + GivenName: &firstName, + FamilyName: &lastName, + Picture: &profilePicture, + Email: emailAddress, + } + + return user, nil +} diff --git a/server/handlers/oauth_login.go b/server/handlers/oauth_login.go index ca8e628..f326928 100644 --- a/server/handlers/oauth_login.go +++ b/server/handlers/oauth_login.go @@ -151,6 +151,23 @@ func OAuthLoginHandler() gin.HandlerFunc { oauth.OAuthProviders.FacebookConfig.RedirectURL = hostname + "/oauth_callback/facebook" url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString) c.Redirect(http.StatusTemporaryRedirect, url) + case constants.SignupMethodLinkedIn: + if oauth.OAuthProviders.LinkedInConfig == nil { + log.Debug("Linkedin OAuth provider is not configured") + isProviderConfigured = false + break + } + err := memorystore.Provider.SetState(oauthStateString, constants.SignupMethodLinkedIn) + if err != nil { + log.Debug("Error setting state: ", err) + c.JSON(500, gin.H{ + "error": "internal server error", + }) + return + } + oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/linkedin" + url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString) + c.Redirect(http.StatusTemporaryRedirect, url) default: log.Debug("Invalid oauth provider: ", provider) c.JSON(422, gin.H{ diff --git a/server/oauth/oauth.go b/server/oauth/oauth.go index 27bfa69..5e43f5b 100644 --- a/server/oauth/oauth.go +++ b/server/oauth/oauth.go @@ -7,6 +7,7 @@ import ( "golang.org/x/oauth2" facebookOAuth2 "golang.org/x/oauth2/facebook" githubOAuth2 "golang.org/x/oauth2/github" + linkedInOAuth2 "golang.org/x/oauth2/linkedin" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/memorystore" @@ -17,6 +18,7 @@ type OAuthProvider struct { GoogleConfig *oauth2.Config GithubConfig *oauth2.Config FacebookConfig *oauth2.Config + LinkedInConfig *oauth2.Config } // OIDCProviders is a struct that contains reference all the OpenID providers @@ -92,5 +94,23 @@ func InitOAuth() error { } } + linkedInClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientID) + if err != nil { + linkedInClientID = "" + } + linkedInClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientSecret) + if err != nil { + linkedInClientSecret = "" + } + if linkedInClientID != "" && linkedInClientSecret != "" { + OAuthProviders.LinkedInConfig = &oauth2.Config{ + ClientID: linkedInClientID, + ClientSecret: linkedInClientSecret, + RedirectURL: "/oauth_callback/linkedin", + Endpoint: linkedInOAuth2.Endpoint, + Scopes: []string{"r_liteprofile", "r_emailaddress"}, + } + } + return nil } diff --git a/server/resolvers/env.go b/server/resolvers/env.go index d63aff4..15c2653 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -130,6 +130,12 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { if val, ok := store[constants.EnvKeyGithubClientSecret]; ok { res.GithubClientSecret = utils.NewStringRef(val.(string)) } + if val, ok := store[constants.EnvKeyLinkedInClientID]; ok { + res.LinkedinClientID = utils.NewStringRef(val.(string)) + } + if val, ok := store[constants.EnvKeyLinkedInClientSecret]; ok { + res.LinkedinClientSecret = utils.NewStringRef(val.(string)) + } if val, ok := store[constants.EnvKeyOrganizationName]; ok { res.OrganizationName = utils.NewStringRef(val.(string)) } diff --git a/server/resolvers/meta.go b/server/resolvers/meta.go index 18fe561..f17775b 100644 --- a/server/resolvers/meta.go +++ b/server/resolvers/meta.go @@ -41,6 +41,18 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { facebookClientSecret = "" } + linkedClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientID) + if err != nil { + log.Debug("Failed to get Facebook Client ID from environment variable", err) + linkedClientID = "" + } + + linkedInClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientSecret) + if err != nil { + log.Debug("Failed to get Facebook Client Secret from environment variable", err) + linkedInClientSecret = "" + } + githubClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) if err != nil { log.Debug("Failed to get Github Client ID from environment variable", err) @@ -83,6 +95,7 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { IsGoogleLoginEnabled: googleClientID != "" && googleClientSecret != "", IsGithubLoginEnabled: githubClientID != "" && githubClientSecret != "", IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "", + IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "", IsBasicAuthenticationEnabled: !isBasicAuthDisabled, IsEmailVerificationEnabled: !isEmailVerificationDisabled, IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled,