├── .bazelignore ├── .bazelrc ├── .bazelversion ├── .gitignore ├── .settings ├── org.eclipse.core.runtime.prefs └── org.eclipse.jdt.core.prefs ├── .travis.yml ├── BUILD ├── LICENSE-Apache2.0 ├── LICENSE-scribe ├── README.md ├── WORKSPACE ├── bazlets.bzl ├── external_plugin_deps.bzl ├── src ├── main │ ├── java │ │ └── com │ │ │ └── googlesource │ │ │ └── gerrit │ │ │ └── plugins │ │ │ └── oauth │ │ │ ├── AirVantageApi.java │ │ │ ├── AirVantageOAuthService.java │ │ │ ├── AzureActiveDirectoryService.java │ │ │ ├── BitbucketApi.java │ │ │ ├── BitbucketOAuthService.java │ │ │ ├── CasApi.java │ │ │ ├── CasOAuthService.java │ │ │ ├── DexApi.java │ │ │ ├── DexOAuthService.java │ │ │ ├── DisabledOAuthLoginProvider.java │ │ │ ├── Facebook2Api.java │ │ │ ├── FacebookOAuthService.java │ │ │ ├── GitHub2Api.java │ │ │ ├── GitHubOAuthService.java │ │ │ ├── GitLabApi.java │ │ │ ├── GitLabOAuthService.java │ │ │ ├── Google2Api.java │ │ │ ├── GoogleOAuthService.java │ │ │ ├── HttpModule.java │ │ │ ├── InitOAuth.java │ │ │ ├── KeycloakApi.java │ │ │ ├── KeycloakOAuthService.java │ │ │ ├── LemonLDAPApi.java │ │ │ ├── LemonLDAPOAuthService.java │ │ │ ├── Module.java │ │ │ ├── PhabricatorApi.java │ │ │ └── PhabricatorOAuthService.java │ └── resources │ │ └── Documentation │ │ ├── build.md │ │ ├── config.md │ │ └── images │ │ ├── github-1.png │ │ ├── github-2.png │ │ ├── gitlab-1.png │ │ ├── gitlab-2.png │ │ ├── google-1.png │ │ ├── google-2.png │ │ ├── google-3.png │ │ ├── google-4.png │ │ └── google-5.png └── test │ └── java │ └── com │ └── googlesource │ └── gerrit │ └── plugins │ └── oauth │ ├── AirVantageApiTest.java │ ├── BitbucketApiTest.java │ ├── CasApiTest.java │ ├── DexApiTest.java │ ├── Facebook2ApiTest.java │ ├── GitHub2ApiTest.java │ ├── GitLabApiTest.java │ ├── GithubApiUrlTest.java │ ├── Google2ApiTest.java │ ├── KeycloakApiTest.java │ ├── MicrosoftAzureActiveDirectory20ApiTest.java │ └── PhabricatorApiTest.java └── tools ├── BUILD ├── bzl ├── BUILD ├── classpath.bzl ├── junit.bzl ├── maven_jar.bzl └── plugin.bzl ├── eclipse ├── BUILD └── project.sh └── workspace_status.py /.bazelignore: -------------------------------------------------------------------------------- 1 | eclipse-out 2 | -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | build --workspace_status_command="python ./tools/workspace_status.py" 2 | test --build_tests_only 3 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 4.2.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.apt_generated 2 | /.classpath 3 | /.project 4 | /.settings 5 | /bazel-bin 6 | /bazel-gerrit-oauth-provider 7 | /bazel-out 8 | /bazel-testlogs 9 | /eclipse-out 10 | /.idea 11 | *.swp 12 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.runtime.prefs: -------------------------------------------------------------------------------- 1 | #Tue Sep 02 16:59:24 PDT 2008 2 | eclipse.preferences.version=1 3 | line.separator=\n 4 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled 3 | org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore 4 | org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull 5 | org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= 6 | org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault 7 | org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= 8 | org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable 9 | org.eclipse.jdt.core.compiler.annotation.nullable.secondary= 10 | org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled 11 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 12 | org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate 13 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 14 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 15 | org.eclipse.jdt.core.compiler.compliance=1.8 16 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 17 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 18 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 19 | org.eclipse.jdt.core.compiler.doc.comment.support=enabled 20 | org.eclipse.jdt.core.compiler.problem.APILeak=warning 21 | org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore 22 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 23 | org.eclipse.jdt.core.compiler.problem.autoboxing=ignore 24 | org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning 25 | org.eclipse.jdt.core.compiler.problem.deadCode=warning 26 | org.eclipse.jdt.core.compiler.problem.deprecation=warning 27 | org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled 28 | org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled 29 | org.eclipse.jdt.core.compiler.problem.discouragedReference=warning 30 | org.eclipse.jdt.core.compiler.problem.emptyStatement=warning 31 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 32 | org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning 33 | org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning 34 | org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled 35 | org.eclipse.jdt.core.compiler.problem.fieldHiding=warning 36 | org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning 37 | org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning 38 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 39 | org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning 40 | org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled 41 | org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning 42 | org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning 43 | org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore 44 | org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning 45 | org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled 46 | org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled 47 | org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled 48 | org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private 49 | org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore 50 | org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning 51 | org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore 52 | org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore 53 | org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=enabled 54 | org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning 55 | org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore 56 | org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled 57 | org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public 58 | org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag 59 | org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore 60 | org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled 61 | org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled 62 | org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=protected 63 | org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning 64 | org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled 65 | org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning 66 | org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore 67 | org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning 68 | org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning 69 | org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore 70 | org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning 71 | org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning 72 | org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error 73 | org.eclipse.jdt.core.compiler.problem.nullReference=warning 74 | org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error 75 | org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning 76 | org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning 77 | org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore 78 | org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning 79 | org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning 80 | org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore 81 | org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore 82 | org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning 83 | org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning 84 | org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning 85 | org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning 86 | org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore 87 | org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore 88 | org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore 89 | org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled 90 | org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning 91 | org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled 92 | org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled 93 | org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled 94 | org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore 95 | org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning 96 | org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning 97 | org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled 98 | org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning 99 | org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning 100 | org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore 101 | org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning 102 | org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning 103 | org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled 104 | org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning 105 | org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning 106 | org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning 107 | org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore 108 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning 109 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled 110 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled 111 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled 112 | org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore 113 | org.eclipse.jdt.core.compiler.problem.unusedImport=warning 114 | org.eclipse.jdt.core.compiler.problem.unusedLabel=warning 115 | org.eclipse.jdt.core.compiler.problem.unusedLocal=warning 116 | org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore 117 | org.eclipse.jdt.core.compiler.problem.unusedParameter=warning 118 | org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled 119 | org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled 120 | org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled 121 | org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning 122 | org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore 123 | org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning 124 | org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning 125 | org.eclipse.jdt.core.compiler.processAnnotations=enabled 126 | org.eclipse.jdt.core.compiler.source=1.8 127 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | dist: bionic 17 | sudo: required 18 | language: java 19 | 20 | matrix: 21 | include: 22 | - os: linux 23 | jdk: oraclejdk11 24 | 25 | install: true 26 | 27 | before_install: 28 | - OS=linux 29 | - ARCH=x86_64 30 | - V=2.1.0 31 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then OS=darwin; fi 32 | - GH_BASE="https://github.com/bazelbuild/bazel/releases/download/$V" 33 | - GH_ARTIFACT="bazel-$V-installer-$OS-$ARCH.sh" 34 | - URL="$GH_BASE/$GH_ARTIFACT" 35 | - echo $URL 36 | - wget -O install.sh $URL 37 | - chmod +x install.sh 38 | - ./install.sh --user 39 | - rm -f install.sh 40 | 41 | script: 42 | - | 43 | bazel build --verbose_failures :all 44 | bazel test --test_output=errors :gerrit-oauth-provider_tests 45 | 46 | notifications: 47 | email: false 48 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_java//java:defs.bzl", "java_library") 2 | load("//tools/bzl:junit.bzl", "junit_tests") 3 | load( 4 | "//tools/bzl:plugin.bzl", 5 | "PLUGIN_DEPS", 6 | "PLUGIN_TEST_DEPS", 7 | "gerrit_plugin", 8 | ) 9 | 10 | gerrit_plugin( 11 | name = "gerrit-oauth-provider", 12 | srcs = glob(["src/main/java/**/*.java"]), 13 | manifest_entries = [ 14 | "Gerrit-PluginName: gerrit-oauth-provider", 15 | "Gerrit-HttpModule: com.googlesource.gerrit.plugins.oauth.HttpModule", 16 | "Gerrit-InitStep: com.googlesource.gerrit.plugins.oauth.InitOAuth", 17 | "Implementation-Title: Gerrit OAuth authentication provider", 18 | "Implementation-URL: https://github.com/davido/gerrit-oauth-provider", 19 | ], 20 | resources = glob(["src/main/resources/**/*"]), 21 | deps = [ 22 | "@commons-codec//jar:neverlink", 23 | "@jackson-core//jar", 24 | "@jackson-databind//jar", 25 | "@scribejava-apis//jar", 26 | "@scribejava-core//jar", 27 | ], 28 | ) 29 | 30 | junit_tests( 31 | name = "gerrit-oauth-provider_tests", 32 | srcs = glob(["src/test/java/**/*.java"]), 33 | tags = ["oauth"], 34 | deps = [ 35 | ":gerrit-oauth-provider__plugin_test_deps", 36 | "@scribejava-apis//jar", 37 | "@scribejava-core//jar", 38 | ], 39 | ) 40 | 41 | java_library( 42 | name = "gerrit-oauth-provider__plugin_test_deps", 43 | testonly = 1, 44 | visibility = ["//visibility:public"], 45 | exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [ 46 | ":gerrit-oauth-provider__plugin", 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /LICENSE-Apache2.0: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-scribe: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Pablo Fernandez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gerrit OAuth2 authentication provider 2 | ===================================== 3 | 4 | [![Build Status](https://travis-ci.org/davido/gerrit-oauth-provider.svg?branch=master)](https://travis-ci.org/davido/gerrit-oauth-provider) 5 | 6 | 7 | With this plugin Gerrit can use OAuth2 protocol for authentication. 8 | Supported OAuth providers: 9 | 10 | * [AirVantage](https://doc.airvantage.net/av/reference/cloud/API/#API-GeneralInformation-Authentication) 11 | * [Bitbucket](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html) 12 | * [CAS](https://www.apereo.org/projects/cas) 13 | * [CoreOS Dex](https://github.com/coreos/dex) 14 | * [Facebook](https://developers.facebook.com/docs/facebook-login) 15 | * [GitHub](https://developer.github.com/v3/oauth/) 16 | * [GitLab](https://about.gitlab.com/) 17 | * [Google](https://developers.google.com/identity/protocols/OAuth2) 18 | * [Keycloak](http://www.keycloak.org/) 19 | * [LemonLDAP::NG](https://lemonldap-ng.org) 20 | * [Azure (previously named Office365)](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols) 21 | * [Phabricator](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/) 22 | 23 | See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you. 24 | 25 | Prebuilt artifacts 26 | ------------------ 27 | 28 | Prebuilt binary artifacts are available on [release page](https://github.com/davido/gerrit-oauth-provider/releases). Make sure to pick the right JAR for your Gerrit version. 29 | 30 | Build 31 | ----- 32 | 33 | To build the plugin with Bazel, install 34 | [Bazel](https://bazel.build/versions/master/docs/install.html) and run the 35 | following: 36 | 37 | ``` 38 | git clone https://gerrit.googlesource.com/plugins/oauth gerrit-oauth-provider 39 | cd gerrit-oauth-provider && bazel build gerrit-oauth-provider 40 | ``` 41 | 42 | Install 43 | ------- 44 | 45 | Copy the `bazel-bin/oauth.jar` to 46 | `$gerrit_site/plugins` and re-run init to configure it: 47 | 48 | ``` 49 | java -jar gerrit.war init -d 50 | [...] 51 | *** OAuth Authentication Provider 52 | *** 53 | Use Bitbucket OAuth provider for Gerrit login ? [Y/n]? n 54 | Use Google OAuth provider for Gerrit login ? [Y/n]? 55 | Application client id : 56 | Application client secret : 57 | confirm password : 58 | Link to OpenID accounts? [true]: 59 | Use GitHub OAuth provider for Gerrit login ? [Y/n]? n 60 | ``` 61 | 62 | Reporting bugs 63 | -------------- 64 | 65 | Make sure to read the [FAQ](https://github.com/davido/gerrit-oauth-provider/wiki/FAQ) before reporting issues. 66 | 67 | License 68 | ------- 69 | 70 | Apache License 2.0 71 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "com_github_davido_gerrit_oauth_provider") 2 | 3 | load("//:bazlets.bzl", "load_bazlets") 4 | 5 | load_bazlets( 6 | commit = "8fa44957c3b3b89ce1d96eba67441882c54503fc", 7 | #local_path = "/home//projects/bazlets", 8 | ) 9 | 10 | load( 11 | "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl", 12 | "gerrit_api", 13 | ) 14 | 15 | gerrit_api() 16 | 17 | load(":external_plugin_deps.bzl", "external_plugin_deps") 18 | 19 | external_plugin_deps(omit_commons_codec = False) 20 | -------------------------------------------------------------------------------- /bazlets.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") 2 | 3 | NAME = "com_googlesource_gerrit_bazlets" 4 | 5 | def load_bazlets( 6 | commit, 7 | local_path = None): 8 | if not local_path: 9 | git_repository( 10 | name = NAME, 11 | remote = "https://gerrit.googlesource.com/bazlets", 12 | commit = commit, 13 | ) 14 | else: 15 | native.local_repository( 16 | name = NAME, 17 | path = local_path, 18 | ) 19 | -------------------------------------------------------------------------------- /external_plugin_deps.bzl: -------------------------------------------------------------------------------- 1 | load("//tools/bzl:maven_jar.bzl", "maven_jar") 2 | 3 | def external_plugin_deps(omit_commons_codec = True): 4 | JACKSON_VERS = "2.10.2" 5 | SCRIBEJAVA_VERS = "6.9.0" 6 | maven_jar( 7 | name = "scribejava-core", 8 | artifact = "com.github.scribejava:scribejava-core:" + SCRIBEJAVA_VERS, 9 | sha1 = "ed761f450d8382f75787e8fee9ae52e7ec768747", 10 | ) 11 | maven_jar( 12 | name = "scribejava-apis", 13 | artifact = "com.github.scribejava:scribejava-apis:" + SCRIBEJAVA_VERS, 14 | sha1 = "a374c7a36533e58e53b42b584a8b3751ab1e13c4", 15 | ) 16 | maven_jar( 17 | name = "jackson-annotations", 18 | artifact = "com.fasterxml.jackson.core:jackson-annotations:" + JACKSON_VERS, 19 | sha1 = "3a13b6105946541b8d4181a0506355b5fae63260", 20 | ) 21 | maven_jar( 22 | name = "jackson-databind", 23 | artifact = "com.fasterxml.jackson.core:jackson-databind:" + JACKSON_VERS, 24 | sha1 = "0528de95f198afafbcfb0c09d2e43b6e0ea663ec", 25 | deps = [ 26 | "@jackson-annotations//jar", 27 | ], 28 | ) 29 | maven_jar( 30 | name = "jackson-core", 31 | artifact = "com.fasterxml.jackson.core:jackson-core:" + JACKSON_VERS, 32 | sha1 = "73d4322a6bda684f676a2b5fe918361c4e5c7cca", 33 | ) 34 | if not omit_commons_codec: 35 | maven_jar( 36 | name = "commons-codec", 37 | artifact = "commons-codec:commons-codec:1.4", 38 | sha1 = "4216af16d38465bbab0f3dff8efa14204f7a399a", 39 | ) 40 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/AirVantageApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2018 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; 19 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; 20 | 21 | public class AirVantageApi extends DefaultApi20 { 22 | @Override 23 | public String getAuthorizationBaseUrl() { 24 | return "https://eu.airvantage.net/api/oauth/authorize"; 25 | } 26 | 27 | @Override 28 | public String getAccessTokenEndpoint() { 29 | return "https://eu.airvantage.net/api/oauth/token"; 30 | } 31 | 32 | @Override 33 | public BearerSignature getBearerSignature() { 34 | return BearerSignatureURIQueryParameter.instance(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/AirVantageOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2018 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | import static javax.servlet.http.HttpServletResponse.SC_OK; 19 | import static org.slf4j.LoggerFactory.getLogger; 20 | 21 | import com.github.scribejava.core.builder.ServiceBuilder; 22 | import com.github.scribejava.core.model.OAuth2AccessToken; 23 | import com.github.scribejava.core.model.OAuthRequest; 24 | import com.github.scribejava.core.model.Response; 25 | import com.github.scribejava.core.model.Verb; 26 | import com.github.scribejava.core.oauth.OAuth20Service; 27 | import com.google.common.base.CharMatcher; 28 | import com.google.gerrit.extensions.annotations.PluginName; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 31 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 32 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 33 | import com.google.gerrit.server.config.CanonicalWebUrl; 34 | import com.google.gerrit.server.config.PluginConfig; 35 | import com.google.gerrit.server.config.PluginConfigFactory; 36 | import com.google.gson.JsonElement; 37 | import com.google.gson.JsonObject; 38 | import com.google.inject.Inject; 39 | import com.google.inject.Provider; 40 | import com.google.inject.Singleton; 41 | import java.io.IOException; 42 | import java.util.concurrent.ExecutionException; 43 | import org.slf4j.Logger; 44 | 45 | @Singleton 46 | public class AirVantageOAuthService implements OAuthServiceProvider { 47 | private static final Logger log = getLogger(AirVantageOAuthService.class); 48 | static final String CONFIG_SUFFIX = "-airvantage-oauth"; 49 | private static final String AV_PROVIDER_PREFIX = "airvantage-oauth:"; 50 | private static final String PROTECTED_RESOURCE_URL = 51 | "https://eu.airvantage.net/api/v1/users/current"; 52 | private final OAuth20Service service; 53 | 54 | @Inject 55 | AirVantageOAuthService( 56 | PluginConfigFactory cfgFactory, 57 | @PluginName String pluginName, 58 | @CanonicalWebUrl Provider urlProvider) { 59 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 60 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 61 | 62 | service = 63 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 64 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 65 | .callback(canonicalWebUrl + "oauth") 66 | .build(new AirVantageApi()); 67 | } 68 | 69 | @Override 70 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 71 | OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL); 72 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 73 | service.signRequest(t, request); 74 | 75 | JsonElement userJson = null; 76 | try (Response response = service.execute(request)) { 77 | if (response.getCode() != SC_OK) { 78 | throw new IOException( 79 | String.format( 80 | "Status %s (%s) for request %s", 81 | response.getCode(), response.getBody(), request.getUrl())); 82 | } 83 | userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 84 | if (log.isDebugEnabled()) { 85 | log.debug("User info response: {}", response.getBody()); 86 | } 87 | if (userJson.isJsonObject()) { 88 | JsonObject jsonObject = userJson.getAsJsonObject(); 89 | JsonElement id = jsonObject.get("uid"); 90 | if (id == null || id.isJsonNull()) { 91 | throw new IOException("Response doesn't contain uid field"); 92 | } 93 | JsonElement email = jsonObject.get("email"); 94 | JsonElement name = jsonObject.get("name"); 95 | return new OAuthUserInfo( 96 | AV_PROVIDER_PREFIX + id.getAsString(), 97 | null, 98 | email.getAsString(), 99 | name.getAsString(), 100 | id.getAsString()); 101 | } 102 | } catch (ExecutionException | InterruptedException e) { 103 | throw new RuntimeException("Cannot retrieve user info resource", e); 104 | } 105 | 106 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 107 | } 108 | 109 | @Override 110 | public OAuthToken getAccessToken(OAuthVerifier rv) { 111 | try { 112 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 113 | return new OAuthToken( 114 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 115 | } catch (InterruptedException | ExecutionException | IOException e) { 116 | String msg = "Cannot retrieve access token"; 117 | log.error(msg, e); 118 | throw new RuntimeException(msg, e); 119 | } 120 | } 121 | 122 | @Override 123 | public String getAuthorizationUrl() { 124 | return service.getAuthorizationUrl(); 125 | } 126 | 127 | @Override 128 | public String getVersion() { 129 | return service.getVersion(); 130 | } 131 | 132 | @Override 133 | public String getName() { 134 | return "AirVantage OAuth2"; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/AzureActiveDirectoryService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2018 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | 19 | import com.github.scribejava.apis.MicrosoftAzureActiveDirectory20Api; 20 | import com.github.scribejava.core.builder.ServiceBuilder; 21 | import com.github.scribejava.core.exceptions.OAuthException; 22 | import com.github.scribejava.core.model.OAuth2AccessToken; 23 | import com.github.scribejava.core.model.OAuthRequest; 24 | import com.github.scribejava.core.model.Response; 25 | import com.github.scribejava.core.model.Verb; 26 | import com.github.scribejava.core.oauth.OAuth20Service; 27 | import com.google.common.base.CharMatcher; 28 | import com.google.common.collect.ImmutableSet; 29 | import com.google.gerrit.extensions.annotations.PluginName; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 31 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 32 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 33 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 34 | import com.google.gerrit.server.config.CanonicalWebUrl; 35 | import com.google.gerrit.server.config.PluginConfig; 36 | import com.google.gerrit.server.config.PluginConfigFactory; 37 | import com.google.gson.Gson; 38 | import com.google.gson.JsonElement; 39 | import com.google.gson.JsonObject; 40 | import com.google.inject.Inject; 41 | import com.google.inject.Provider; 42 | import com.google.inject.Singleton; 43 | import java.io.IOException; 44 | import java.nio.charset.StandardCharsets; 45 | import java.util.Base64; 46 | import java.util.concurrent.ExecutionException; 47 | import javax.servlet.http.HttpServletResponse; 48 | import org.slf4j.Logger; 49 | import org.slf4j.LoggerFactory; 50 | 51 | @Singleton 52 | class AzureActiveDirectoryService implements OAuthServiceProvider { 53 | private static final Logger log = LoggerFactory.getLogger(AzureActiveDirectoryService.class); 54 | static final String CONFIG_SUFFIX_LEGACY = "-office365-oauth"; 55 | static final String CONFIG_SUFFIX = "-azure-oauth"; 56 | private static final String AZURE_PROVIDER_PREFIX = "azure-oauth:"; 57 | private static final String OFFICE365_PROVIDER_PREFIX = "office365-oauth:"; 58 | private static final String PROTECTED_RESOURCE_URL = "https://graph.microsoft.com/v1.0/me"; 59 | private static final String SCOPE = 60 | "openid offline_access https://graph.microsoft.com/user.readbasic.all"; 61 | public static final String DEFAULT_TENANT = "organizations"; 62 | private static final ImmutableSet TENANTS_WITHOUT_VALIDATION = 63 | ImmutableSet.builder().add(DEFAULT_TENANT).add("common").add("consumers").build(); 64 | private final OAuth20Service service; 65 | private final Gson gson; 66 | private final String canonicalWebUrl; 67 | private final boolean useEmailAsUsername; 68 | private final String tenant; 69 | private final String clientId; 70 | private String providerPrefix; 71 | private final boolean linkOffice365Id; 72 | 73 | @Inject 74 | AzureActiveDirectoryService( 75 | PluginConfigFactory cfgFactory, 76 | @PluginName String pluginName, 77 | @CanonicalWebUrl Provider urlProvider) { 78 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 79 | providerPrefix = AZURE_PROVIDER_PREFIX; 80 | 81 | // ?: Did we find the client_id with the CONFIG_SUFFIX 82 | if (cfg.getString(InitOAuth.CLIENT_ID) == null) { 83 | // -> No, we did not find the client_id in the azure config so we should try the old legacy 84 | // office365 section 85 | cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX_LEGACY); 86 | // We must also use the new provider prefix 87 | providerPrefix = OFFICE365_PROVIDER_PREFIX; 88 | } 89 | this.linkOffice365Id = cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_OFFICE365_ACCOUNT, false); 90 | this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 91 | this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false); 92 | this.tenant = cfg.getString(InitOAuth.TENANT, DEFAULT_TENANT); 93 | this.clientId = cfg.getString(InitOAuth.CLIENT_ID); 94 | this.service = 95 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 96 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 97 | .callback(canonicalWebUrl + "oauth") 98 | .defaultScope(SCOPE) 99 | .build(MicrosoftAzureActiveDirectory20Api.custom(tenant)); 100 | this.gson = JSON.newGson(); 101 | if (log.isDebugEnabled()) { 102 | log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl); 103 | log.debug("OAuth2: scope={}", SCOPE); 104 | log.debug("OAuth2: useEmailAsUsername={}", useEmailAsUsername); 105 | } 106 | } 107 | 108 | @Override 109 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 110 | // ?: Have we set a custom tenant and is this a tenant other than the one set in 111 | // TENANTS_WITHOUT_VALIDATION 112 | if (!TENANTS_WITHOUT_VALIDATION.contains(tenant)) { 113 | // -> Yes, we are using a tenant that should be validated, so verify that is issued for the 114 | // same one that we 115 | // have set. 116 | String tid = getTokenJson(token.getToken()).get("tid").getAsString(); 117 | 118 | // ?: Verify that this token has the same tenant as we are currently using 119 | if (!tenant.equals(tid)) { 120 | // -> No, this tenant does not equals the one in the token. So we should stop processing 121 | log.warn( 122 | String.format( 123 | "The token was issued by the tenant [%s] while we are set to use [%s]", 124 | tid, tenant)); 125 | // Return null so the user will be shown Unauthorized. 126 | return null; 127 | } 128 | } 129 | 130 | // Due to scribejava does not expose the id_token we need to do this a bit convoluted way to 131 | // extract this our self 132 | // see Obtaining id_token from 133 | // access_token for 134 | // the scribejava issue on this. 135 | String rawToken = token.getRaw(); 136 | JsonObject jwtJson = gson.fromJson(rawToken, JsonObject.class); 137 | String idTokenBase64 = jwtJson.get("id_token").getAsString(); 138 | String aud = getTokenJson(idTokenBase64).get("aud").getAsString(); 139 | 140 | // ?: Does this token have the same clientId set in the 'aud' part of the id_token as we are 141 | // using. 142 | // If not we should reject it 143 | // see id 144 | // tokens Payload claims> 145 | // for information on the aud claim. 146 | if (!clientId.equals(aud)) { 147 | log.warn( 148 | String.format( 149 | "The id_token had aud [%s] while we expected it to be equal to the clientId [%s]", 150 | aud, clientId)); 151 | // Return null so the user will be shown Unauthorized. 152 | return null; 153 | } 154 | 155 | OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL); 156 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 157 | service.signRequest(t, request); 158 | request.addHeader("Accept", "*/*"); 159 | 160 | JsonElement userJson = null; 161 | try (Response response = service.execute(request)) { 162 | if (response.getCode() != HttpServletResponse.SC_OK) { 163 | throw new IOException( 164 | String.format( 165 | "Status %s (%s) for request %s", 166 | response.getCode(), response.getBody(), request.getUrl())); 167 | } 168 | userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 169 | if (log.isDebugEnabled()) { 170 | log.debug("User info response: {}", response.getBody()); 171 | } 172 | if (userJson.isJsonObject()) { 173 | JsonObject jsonObject = userJson.getAsJsonObject(); 174 | JsonElement id = jsonObject.get("id"); 175 | if (id == null || id.isJsonNull()) { 176 | throw new IOException("Response doesn't contain id field"); 177 | } 178 | JsonElement email = jsonObject.get("mail"); 179 | JsonElement name = jsonObject.get("displayName"); 180 | String login = null; 181 | 182 | if (useEmailAsUsername && !email.isJsonNull()) { 183 | login = email.getAsString().split("@")[0]; 184 | } 185 | 186 | return new OAuthUserInfo( 187 | providerPrefix + id.getAsString() /*externalId*/, 188 | login /*username*/, 189 | email == null || email.isJsonNull() ? null : email.getAsString() /*email*/, 190 | name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/, 191 | linkOffice365Id ? OFFICE365_PROVIDER_PREFIX + id.getAsString() : null); 192 | } 193 | } catch (ExecutionException | InterruptedException e) { 194 | throw new RuntimeException("Cannot retrieve user info resource", e); 195 | } 196 | 197 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 198 | } 199 | 200 | @Override 201 | public OAuthToken getAccessToken(OAuthVerifier rv) { 202 | try { 203 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 204 | return new OAuthToken( 205 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 206 | } catch (InterruptedException | ExecutionException | IOException e) { 207 | String msg = "Cannot retrieve access token"; 208 | log.error(msg, e); 209 | throw new RuntimeException(msg, e); 210 | } 211 | } 212 | 213 | @Override 214 | public String getAuthorizationUrl() { 215 | String url = service.getAuthorizationUrl(); 216 | return url; 217 | } 218 | 219 | @Override 220 | public String getVersion() { 221 | return service.getVersion(); 222 | } 223 | 224 | @Override 225 | public String getName() { 226 | return "Office365 OAuth2"; 227 | } 228 | 229 | /** Get the {@link JsonObject} of a given token. */ 230 | private JsonObject getTokenJson(String tokenBase64) { 231 | String[] tokenParts = tokenBase64.split("\\."); 232 | if (tokenParts.length != 3) { 233 | throw new OAuthException("Token does not contain expected number of parts"); 234 | } 235 | 236 | // Extract the payload part from the JWT token (header.payload.signature) by retrieving 237 | // tokenParts[1]. 238 | return gson.fromJson( 239 | new String(Base64.getUrlDecoder().decode(tokenParts[1]), StandardCharsets.UTF_8), 240 | JsonObject.class); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; 19 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; 20 | 21 | public class BitbucketApi extends DefaultApi20 { 22 | @Override 23 | public String getAuthorizationBaseUrl() { 24 | return "https://bitbucket.org/site/oauth2/authorize"; 25 | } 26 | 27 | @Override 28 | public String getAccessTokenEndpoint() { 29 | return "https://bitbucket.org/site/oauth2/access_token"; 30 | } 31 | 32 | @Override 33 | public BearerSignature getBearerSignature() { 34 | return BearerSignatureURIQueryParameter.instance(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | import static javax.servlet.http.HttpServletResponse.SC_OK; 19 | import static org.slf4j.LoggerFactory.getLogger; 20 | 21 | import com.github.scribejava.core.builder.ServiceBuilder; 22 | import com.github.scribejava.core.model.OAuth2AccessToken; 23 | import com.github.scribejava.core.model.OAuthRequest; 24 | import com.github.scribejava.core.model.Response; 25 | import com.github.scribejava.core.model.Verb; 26 | import com.github.scribejava.core.oauth.OAuth20Service; 27 | import com.google.common.base.CharMatcher; 28 | import com.google.gerrit.extensions.annotations.PluginName; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 31 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 32 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 33 | import com.google.gerrit.server.config.CanonicalWebUrl; 34 | import com.google.gerrit.server.config.PluginConfig; 35 | import com.google.gerrit.server.config.PluginConfigFactory; 36 | import com.google.gson.JsonElement; 37 | import com.google.gson.JsonObject; 38 | import com.google.inject.Inject; 39 | import com.google.inject.Provider; 40 | import com.google.inject.Singleton; 41 | import java.io.IOException; 42 | import java.util.concurrent.ExecutionException; 43 | import org.slf4j.Logger; 44 | 45 | @Singleton 46 | public class BitbucketOAuthService implements OAuthServiceProvider { 47 | private static final Logger log = getLogger(BitbucketOAuthService.class); 48 | static final String CONFIG_SUFFIX = "-bitbucket-oauth"; 49 | private static final String BITBUCKET_PROVIDER_PREFIX = "bitbucket-oauth:"; 50 | private static final String PROTECTED_RESOURCE_URL = "https://bitbucket.org/api/1.0/user/"; 51 | private final boolean fixLegacyUserId; 52 | private final OAuth20Service service; 53 | 54 | @Inject 55 | BitbucketOAuthService( 56 | PluginConfigFactory cfgFactory, 57 | @PluginName String pluginName, 58 | @CanonicalWebUrl Provider urlProvider) { 59 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 60 | 61 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 62 | fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false); 63 | service = 64 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 65 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 66 | .callback(canonicalWebUrl + "oauth") 67 | .build(new BitbucketApi()); 68 | } 69 | 70 | @Override 71 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 72 | OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL); 73 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 74 | service.signRequest(t, request); 75 | 76 | JsonElement userJson = null; 77 | try (Response response = service.execute(request)) { 78 | 79 | if (response.getCode() != SC_OK) { 80 | throw new IOException( 81 | String.format( 82 | "Status %s (%s) for request %s", 83 | response.getCode(), response.getBody(), request.getUrl())); 84 | } 85 | userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 86 | if (log.isDebugEnabled()) { 87 | log.debug("User info response: {}", response.getBody()); 88 | } 89 | if (userJson.isJsonObject()) { 90 | JsonObject jsonObject = userJson.getAsJsonObject(); 91 | JsonObject userObject = jsonObject.getAsJsonObject("user"); 92 | if (userObject == null || userObject.isJsonNull()) { 93 | throw new IOException("Response doesn't contain 'user' field"); 94 | } 95 | JsonElement usernameElement = userObject.get("username"); 96 | String username = usernameElement.getAsString(); 97 | 98 | JsonElement displayName = jsonObject.get("display_name"); 99 | return new OAuthUserInfo( 100 | BITBUCKET_PROVIDER_PREFIX + username, 101 | username, 102 | null, 103 | displayName == null || displayName.isJsonNull() ? null : displayName.getAsString(), 104 | fixLegacyUserId ? username : null); 105 | } 106 | } catch (ExecutionException | InterruptedException e) { 107 | throw new RuntimeException("Cannot retrieve user info resource", e); 108 | } 109 | 110 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 111 | } 112 | 113 | @Override 114 | public OAuthToken getAccessToken(OAuthVerifier rv) { 115 | try { 116 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 117 | return new OAuthToken( 118 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 119 | } catch (InterruptedException | ExecutionException | IOException e) { 120 | String msg = "Cannot retrieve access token"; 121 | log.error(msg, e); 122 | throw new RuntimeException(msg, e); 123 | } 124 | } 125 | 126 | @Override 127 | public String getAuthorizationUrl() { 128 | return service.getAuthorizationUrl(); 129 | } 130 | 131 | @Override 132 | public String getVersion() { 133 | return service.getVersion(); 134 | } 135 | 136 | @Override 137 | public String getName() { 138 | return "Bitbucket OAuth2"; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor; 19 | import com.github.scribejava.core.extractors.TokenExtractor; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; 22 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; 23 | 24 | public class CasApi extends DefaultApi20 { 25 | private static final String AUTHORIZE_URL = "%s/oauth2.0/authorize"; 26 | 27 | private final String rootUrl; 28 | 29 | public CasApi(String rootUrl) { 30 | this.rootUrl = rootUrl; 31 | } 32 | 33 | @Override 34 | public String getAccessTokenEndpoint() { 35 | return String.format("%s/oauth2.0/accessToken", rootUrl); 36 | } 37 | 38 | @Override 39 | public String getAuthorizationBaseUrl() { 40 | return String.format(AUTHORIZE_URL, rootUrl); 41 | } 42 | 43 | @Override 44 | public BearerSignature getBearerSignature() { 45 | return BearerSignatureURIQueryParameter.instance(); 46 | } 47 | 48 | @Override 49 | public TokenExtractor getAccessTokenExtractor() { 50 | return OAuth2AccessTokenExtractor.instance(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.base.Strings.nullToEmpty; 18 | import static com.google.gerrit.json.OutputFormat.JSON; 19 | 20 | import com.github.scribejava.core.builder.ServiceBuilder; 21 | import com.github.scribejava.core.model.OAuth2AccessToken; 22 | import com.github.scribejava.core.model.OAuthRequest; 23 | import com.github.scribejava.core.model.Response; 24 | import com.github.scribejava.core.model.Verb; 25 | import com.github.scribejava.core.oauth.OAuth20Service; 26 | import com.google.common.base.CharMatcher; 27 | import com.google.gerrit.extensions.annotations.PluginName; 28 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 31 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 32 | import com.google.gerrit.server.config.CanonicalWebUrl; 33 | import com.google.gerrit.server.config.PluginConfig; 34 | import com.google.gerrit.server.config.PluginConfigFactory; 35 | import com.google.gson.JsonArray; 36 | import com.google.gson.JsonElement; 37 | import com.google.gson.JsonObject; 38 | import com.google.inject.Inject; 39 | import com.google.inject.Provider; 40 | import com.google.inject.ProvisionException; 41 | import com.google.inject.Singleton; 42 | import java.io.IOException; 43 | import java.net.URI; 44 | import java.util.concurrent.ExecutionException; 45 | import javax.servlet.http.HttpServletResponse; 46 | import org.slf4j.Logger; 47 | import org.slf4j.LoggerFactory; 48 | 49 | @Singleton 50 | class CasOAuthService implements OAuthServiceProvider { 51 | private static final Logger log = LoggerFactory.getLogger(CasOAuthService.class); 52 | static final String CONFIG_SUFFIX = "-cas-oauth"; 53 | private static final String CAS_PROVIDER_PREFIX = "cas-oauth:"; 54 | private static final String PROTECTED_RESOURCE_URL = "%s/oauth2.0/profile"; 55 | 56 | private final String rootUrl; 57 | private final boolean fixLegacyUserId; 58 | private final OAuth20Service service; 59 | 60 | @Inject 61 | CasOAuthService( 62 | PluginConfigFactory cfgFactory, 63 | @PluginName String pluginName, 64 | @CanonicalWebUrl Provider urlProvider) { 65 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 66 | rootUrl = cfg.getString(InitOAuth.ROOT_URL); 67 | if (!URI.create(rootUrl).isAbsolute()) { 68 | throw new ProvisionException("Root URL must be absolute URL"); 69 | } 70 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 71 | fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false); 72 | service = 73 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 74 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 75 | .callback(canonicalWebUrl + "oauth") 76 | .build(new CasApi(rootUrl)); 77 | } 78 | 79 | @Override 80 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 81 | OAuthRequest request = 82 | new OAuthRequest(Verb.GET, String.format(PROTECTED_RESOURCE_URL, rootUrl)); 83 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 84 | service.signRequest(t, request); 85 | 86 | try (Response response = service.execute(request)) { 87 | if (response.getCode() != HttpServletResponse.SC_OK) { 88 | throw new IOException( 89 | String.format( 90 | "Status %s (%s) for request %s", 91 | response.getCode(), response.getBody(), request.getUrl())); 92 | } 93 | 94 | if (log.isDebugEnabled()) { 95 | log.debug("User info response: {}", response.getBody()); 96 | } 97 | 98 | JsonElement userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 99 | if (!userJson.isJsonObject()) { 100 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 101 | } 102 | JsonObject jsonObject = userJson.getAsJsonObject(); 103 | 104 | JsonElement id = jsonObject.get("id"); 105 | if (id == null || id.isJsonNull()) { 106 | throw new IOException(String.format("CAS response missing id: %s", response.getBody())); 107 | } 108 | 109 | JsonElement attrListJson = jsonObject.get("attributes"); 110 | if (attrListJson == null) { 111 | throw new IOException( 112 | String.format("CAS response missing attributes: %s", response.getBody())); 113 | } 114 | 115 | String email = null, name = null, login = null; 116 | if (attrListJson.isJsonArray()) { 117 | // It is possible for CAS to be configured to not return any attributes (email, name, 118 | // login), 119 | // in which case, 120 | // CAS returns an empty JSON object "attributes":{}, rather than "null" or an empty JSON 121 | // array 122 | // "attributes": [] 123 | 124 | JsonArray attrJson = attrListJson.getAsJsonArray(); 125 | for (JsonElement elem : attrJson) { 126 | if (elem == null || !elem.isJsonObject()) { 127 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", elem)); 128 | } 129 | JsonObject obj = elem.getAsJsonObject(); 130 | 131 | String property = getStringElement(obj, "email"); 132 | if (property != null) { 133 | email = property; 134 | } 135 | property = getStringElement(obj, "name"); 136 | if (property != null) { 137 | name = property; 138 | } 139 | property = getStringElement(obj, "login"); 140 | if (property != null) { 141 | login = property; 142 | } 143 | } 144 | } 145 | 146 | return new OAuthUserInfo( 147 | CAS_PROVIDER_PREFIX + id.getAsString(), 148 | login, 149 | email, 150 | name, 151 | fixLegacyUserId ? id.getAsString() : null); 152 | } catch (ExecutionException | InterruptedException e) { 153 | throw new RuntimeException("Cannot retrieve user info resource", e); 154 | } 155 | } 156 | 157 | private String getStringElement(JsonObject o, String name) { 158 | JsonElement elem = o.get(name); 159 | if (elem == null || elem.isJsonNull()) { 160 | return null; 161 | } 162 | 163 | return elem.getAsString(); 164 | } 165 | 166 | @Override 167 | public OAuthToken getAccessToken(OAuthVerifier rv) { 168 | try { 169 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 170 | return new OAuthToken( 171 | accessToken.getAccessToken(), 172 | nullToEmpty(accessToken.getTokenType()), 173 | accessToken.getRawResponse()); 174 | } catch (InterruptedException | ExecutionException | IOException e) { 175 | String msg = "Cannot retrieve access token"; 176 | log.error(msg, e); 177 | throw new RuntimeException(msg, e); 178 | } 179 | } 180 | 181 | @Override 182 | public String getAuthorizationUrl() { 183 | return service.getAuthorizationUrl(); 184 | } 185 | 186 | @Override 187 | public String getVersion() { 188 | return service.getVersion(); 189 | } 190 | 191 | @Override 192 | public String getName() { 193 | return "Generic CAS OAuth2"; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/DexApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; 19 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; 20 | 21 | public class DexApi extends DefaultApi20 { 22 | 23 | private static final String AUTHORIZE_URL = "%s/dex/auth"; 24 | 25 | private final String rootUrl; 26 | 27 | public DexApi(String rootUrl) { 28 | this.rootUrl = rootUrl; 29 | } 30 | 31 | @Override 32 | public String getAuthorizationBaseUrl() { 33 | return String.format(AUTHORIZE_URL, rootUrl); 34 | } 35 | 36 | @Override 37 | public String getAccessTokenEndpoint() { 38 | return String.format("%s/dex/token", rootUrl); 39 | } 40 | 41 | @Override 42 | public BearerSignature getBearerSignature() { 43 | return BearerSignatureURIQueryParameter.instance(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/DexOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | 19 | import com.github.scribejava.core.builder.ServiceBuilder; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | import com.github.scribejava.core.oauth.OAuth20Service; 22 | import com.google.common.base.CharMatcher; 23 | import com.google.common.base.Preconditions; 24 | import com.google.gerrit.extensions.annotations.PluginName; 25 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 26 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 27 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 28 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 29 | import com.google.gerrit.server.config.CanonicalWebUrl; 30 | import com.google.gerrit.server.config.PluginConfig; 31 | import com.google.gerrit.server.config.PluginConfigFactory; 32 | import com.google.gson.JsonElement; 33 | import com.google.gson.JsonObject; 34 | import com.google.inject.Inject; 35 | import com.google.inject.Provider; 36 | import com.google.inject.ProvisionException; 37 | import com.google.inject.Singleton; 38 | import java.io.IOException; 39 | import java.io.UnsupportedEncodingException; 40 | import java.net.URI; 41 | import java.nio.charset.StandardCharsets; 42 | import java.util.concurrent.ExecutionException; 43 | import org.apache.commons.codec.binary.Base64; 44 | import org.slf4j.Logger; 45 | import org.slf4j.LoggerFactory; 46 | 47 | @Singleton 48 | public class DexOAuthService implements OAuthServiceProvider { 49 | private static final Logger log = LoggerFactory.getLogger(DexOAuthService.class); 50 | 51 | static final String CONFIG_SUFFIX = "-dex-oauth"; 52 | private static final String DEX_PROVIDER_PREFIX = "dex-oauth:"; 53 | private final OAuth20Service service; 54 | private final String rootUrl; 55 | private final String domain; 56 | private final String serviceName; 57 | 58 | @Inject 59 | DexOAuthService( 60 | PluginConfigFactory cfgFactory, 61 | @PluginName String pluginName, 62 | @CanonicalWebUrl Provider urlProvider) { 63 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 64 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 65 | 66 | rootUrl = cfg.getString(InitOAuth.ROOT_URL); 67 | if (!URI.create(rootUrl).isAbsolute()) { 68 | throw new ProvisionException("Root URL must be absolute URL"); 69 | } 70 | domain = cfg.getString(InitOAuth.DOMAIN, null); 71 | serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Dex OAuth2"); 72 | 73 | service = 74 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 75 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 76 | .defaultScope("openid profile email offline_access") 77 | .callback(canonicalWebUrl + "oauth") 78 | .build(new DexApi(rootUrl)); 79 | } 80 | 81 | private String parseJwt(String input) throws UnsupportedEncodingException { 82 | String[] parts = input.split("\\."); 83 | Preconditions.checkState(parts.length == 3); 84 | Preconditions.checkNotNull(parts[1]); 85 | return new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8.name()); 86 | } 87 | 88 | @Override 89 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 90 | JsonElement tokenJson = JSON.newGson().fromJson(token.getRaw(), JsonElement.class); 91 | JsonObject tokenObject = tokenJson.getAsJsonObject(); 92 | JsonElement id_token = tokenObject.get("id_token"); 93 | 94 | String jwt; 95 | try { 96 | jwt = parseJwt(id_token.getAsString()); 97 | } catch (UnsupportedEncodingException e) { 98 | throw new IOException( 99 | String.format( 100 | "%s support is required to interact with JWTs", StandardCharsets.UTF_8.name()), 101 | e); 102 | } 103 | 104 | JsonElement claimJson = JSON.newGson().fromJson(jwt, JsonElement.class); 105 | 106 | // Dex does not support basic profile currently (2017-09), extracting info 107 | // from access token claim 108 | 109 | JsonObject claimObject = claimJson.getAsJsonObject(); 110 | JsonElement emailElement = claimObject.get("email"); 111 | JsonElement nameElement = claimObject.get("name"); 112 | if (emailElement == null || emailElement.isJsonNull()) { 113 | throw new IOException("Response doesn't contain email field"); 114 | } 115 | if (nameElement == null || nameElement.isJsonNull()) { 116 | throw new IOException("Response doesn't contain name field"); 117 | } 118 | String email = emailElement.getAsString(); 119 | String name = nameElement.getAsString(); 120 | String username = email; 121 | if (domain != null && domain.length() > 0) { 122 | username = email.replace("@" + domain, ""); 123 | } 124 | 125 | return new OAuthUserInfo( 126 | DEX_PROVIDER_PREFIX + email /*externalId*/, 127 | username /*username*/, 128 | email /*email*/, 129 | name /*displayName*/, 130 | null /*claimedIdentity*/); 131 | } 132 | 133 | @Override 134 | public OAuthToken getAccessToken(OAuthVerifier rv) { 135 | try { 136 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 137 | return new OAuthToken( 138 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 139 | } catch (InterruptedException | ExecutionException | IOException e) { 140 | String msg = "Cannot retrieve access token"; 141 | log.error(msg, e); 142 | throw new RuntimeException(msg, e); 143 | } 144 | } 145 | 146 | @Override 147 | public String getAuthorizationUrl() { 148 | return service.getAuthorizationUrl(); 149 | } 150 | 151 | @Override 152 | public String getVersion() { 153 | return service.getVersion(); 154 | } 155 | 156 | @Override 157 | public String getName() { 158 | return serviceName; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/DisabledOAuthLoginProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.google.gerrit.extensions.annotations.PluginName; 18 | import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider; 19 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 20 | import com.google.inject.Inject; 21 | import com.google.inject.Singleton; 22 | import java.io.IOException; 23 | 24 | @Singleton 25 | class DisabledOAuthLoginProvider implements OAuthLoginProvider { 26 | private final String pluginName; 27 | 28 | @Inject 29 | DisabledOAuthLoginProvider(@PluginName String pluginName) { 30 | this.pluginName = pluginName; 31 | } 32 | 33 | @Override 34 | public OAuthUserInfo login(String username, String secret) throws IOException { 35 | throw new UnsupportedOperationException( 36 | "git over oauth is not implemented by " + pluginName + " plugin"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/Facebook2Api.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2018 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication; 19 | import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme; 20 | 21 | public class Facebook2Api extends DefaultApi20 { 22 | @Override 23 | protected String getAuthorizationBaseUrl() { 24 | return "https://www.facebook.com/dialog/oauth"; 25 | } 26 | 27 | @Override 28 | public String getAccessTokenEndpoint() { 29 | return "https://graph.facebook.com/oauth/access_token"; 30 | } 31 | 32 | @Override 33 | public ClientAuthentication getClientAuthentication() { 34 | return RequestBodyAuthenticationScheme.instance(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/FacebookOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | 19 | import com.github.scribejava.core.builder.ServiceBuilder; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | import com.github.scribejava.core.model.OAuthRequest; 22 | import com.github.scribejava.core.model.Response; 23 | import com.github.scribejava.core.model.Verb; 24 | import com.github.scribejava.core.oauth.OAuth20Service; 25 | import com.google.common.base.CharMatcher; 26 | import com.google.gerrit.extensions.annotations.PluginName; 27 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 28 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 31 | import com.google.gerrit.server.config.CanonicalWebUrl; 32 | import com.google.gerrit.server.config.PluginConfig; 33 | import com.google.gerrit.server.config.PluginConfigFactory; 34 | import com.google.gson.JsonElement; 35 | import com.google.gson.JsonObject; 36 | import com.google.inject.Inject; 37 | import com.google.inject.Provider; 38 | import com.google.inject.Singleton; 39 | import java.io.IOException; 40 | import java.util.concurrent.ExecutionException; 41 | import javax.servlet.http.HttpServletResponse; 42 | import org.slf4j.Logger; 43 | import org.slf4j.LoggerFactory; 44 | 45 | @Singleton 46 | class FacebookOAuthService implements OAuthServiceProvider { 47 | private static final Logger log = LoggerFactory.getLogger(FacebookOAuthService.class); 48 | static final String CONFIG_SUFFIX = "-facebook-oauth"; 49 | private static final String PROTECTED_RESOURCE_URL = "https://graph.facebook.com/me"; 50 | 51 | private static final String FACEBOOK_PROVIDER_PREFIX = "facebook-oauth:"; 52 | private static final String SCOPE = "email"; 53 | private static final String FIELDS_QUERY = "fields"; 54 | private static final String FIELDS = "email,name"; 55 | private final OAuth20Service service; 56 | 57 | @Inject 58 | FacebookOAuthService( 59 | PluginConfigFactory cfgFactory, 60 | @PluginName String pluginName, 61 | @CanonicalWebUrl Provider urlProvider) { 62 | 63 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 64 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 65 | 66 | service = 67 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 68 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 69 | .callback(canonicalWebUrl + "oauth") 70 | .defaultScope(SCOPE) 71 | .build(new Facebook2Api()); 72 | } 73 | 74 | @Override 75 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 76 | OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL); 77 | request.addQuerystringParameter(FIELDS_QUERY, FIELDS); 78 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 79 | service.signRequest(t, request); 80 | 81 | JsonElement userJson = null; 82 | try (Response response = service.execute(request)) { 83 | if (response.getCode() != HttpServletResponse.SC_OK) { 84 | throw new IOException( 85 | String.format( 86 | "Status %s (%s) for request %s", 87 | response.getCode(), response.getBody(), request.getUrl())); 88 | } 89 | userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 90 | 91 | if (log.isDebugEnabled()) { 92 | log.debug("User info response: {}", response.getBody()); 93 | } 94 | if (userJson.isJsonObject()) { 95 | JsonObject jsonObject = userJson.getAsJsonObject(); 96 | JsonElement id = jsonObject.get("id"); 97 | if (id == null || id.isJsonNull()) { 98 | throw new IOException("Response doesn't contain id field"); 99 | } 100 | JsonElement email = jsonObject.get("email"); 101 | JsonElement name = jsonObject.get("name"); 102 | // Heads up! 103 | // Lets keep `login` equal to `email`, since `username` field is 104 | // deprecated for Facebook API versions v2.0 and higher 105 | JsonElement login = jsonObject.get("email"); 106 | 107 | return new OAuthUserInfo( 108 | FACEBOOK_PROVIDER_PREFIX + id.getAsString(), 109 | login == null || login.isJsonNull() ? null : login.getAsString(), 110 | email == null || email.isJsonNull() ? null : email.getAsString(), 111 | name == null || name.isJsonNull() ? null : name.getAsString(), 112 | null); 113 | } 114 | } catch (ExecutionException | InterruptedException e) { 115 | throw new RuntimeException("Cannot retrieve user info resource", e); 116 | } 117 | 118 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 119 | } 120 | 121 | @Override 122 | public OAuthToken getAccessToken(OAuthVerifier rv) { 123 | try { 124 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 125 | return new OAuthToken( 126 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 127 | } catch (InterruptedException | ExecutionException | IOException e) { 128 | String msg = "Cannot retrieve access token"; 129 | log.error(msg, e); 130 | throw new RuntimeException(msg, e); 131 | } 132 | } 133 | 134 | @Override 135 | public String getAuthorizationUrl() { 136 | return service.getAuthorizationUrl(); 137 | } 138 | 139 | @Override 140 | public String getVersion() { 141 | return service.getVersion(); 142 | } 143 | 144 | @Override 145 | public String getName() { 146 | return "Facebook OAuth2"; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/GitHub2Api.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor; 19 | import com.github.scribejava.core.extractors.TokenExtractor; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | 22 | public class GitHub2Api extends DefaultApi20 { 23 | private static final String AUTHORIZE_URL = "%slogin/oauth/authorize"; 24 | 25 | private final String rootUrl; 26 | 27 | public GitHub2Api(String rootUrl) { 28 | this.rootUrl = rootUrl; 29 | } 30 | 31 | @Override 32 | public String getAccessTokenEndpoint() { 33 | return String.format("%slogin/oauth/access_token", rootUrl); 34 | } 35 | 36 | @Override 37 | protected String getAuthorizationBaseUrl() { 38 | return String.format(AUTHORIZE_URL, rootUrl); 39 | } 40 | 41 | @Override 42 | public TokenExtractor getAccessTokenExtractor() { 43 | return OAuth2AccessTokenExtractor.instance(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/GitHubOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | 19 | import com.github.scribejava.core.builder.ServiceBuilder; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | import com.github.scribejava.core.model.OAuthRequest; 22 | import com.github.scribejava.core.model.Response; 23 | import com.github.scribejava.core.model.Verb; 24 | import com.github.scribejava.core.oauth.OAuth20Service; 25 | import com.google.common.base.CharMatcher; 26 | import com.google.gerrit.extensions.annotations.PluginName; 27 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 28 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 31 | import com.google.gerrit.server.config.CanonicalWebUrl; 32 | import com.google.gerrit.server.config.PluginConfig; 33 | import com.google.gerrit.server.config.PluginConfigFactory; 34 | import com.google.gson.JsonElement; 35 | import com.google.gson.JsonObject; 36 | import com.google.inject.Inject; 37 | import com.google.inject.Provider; 38 | import com.google.inject.Singleton; 39 | import java.io.IOException; 40 | import java.util.concurrent.ExecutionException; 41 | import javax.servlet.http.HttpServletResponse; 42 | import org.slf4j.Logger; 43 | import org.slf4j.LoggerFactory; 44 | 45 | @Singleton 46 | class GitHubOAuthService implements OAuthServiceProvider { 47 | private static final Logger log = LoggerFactory.getLogger(GitHubOAuthService.class); 48 | static final String CONFIG_SUFFIX = "-github-oauth"; 49 | private static final String GITHUB_PROVIDER_PREFIX = "github-oauth:"; 50 | private static final String GITHUB_API_ENDPOINT_URL = "https://api.github.com/"; 51 | private static final String GHE_API_ENDPOINT_URL = "%sapi/v3/"; 52 | static final String GITHUB_ROOT_URL = "https://github.com/"; 53 | private final String rootUrl; 54 | 55 | static final String SCOPE = "user:email"; 56 | private final boolean fixLegacyUserId; 57 | private final OAuth20Service service; 58 | 59 | @Inject 60 | GitHubOAuthService( 61 | PluginConfigFactory cfgFactory, 62 | @PluginName String pluginName, 63 | @CanonicalWebUrl Provider urlProvider) { 64 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 65 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 66 | fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false); 67 | rootUrl = 68 | CharMatcher.is('/').trimTrailingFrom(cfg.getString(InitOAuth.ROOT_URL, GITHUB_ROOT_URL)) 69 | + "/"; 70 | 71 | service = 72 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 73 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 74 | .callback(canonicalWebUrl + "oauth") 75 | .defaultScope(SCOPE) 76 | .build(new GitHub2Api(rootUrl)); 77 | } 78 | 79 | private String getApiUrl() { 80 | return GITHUB_ROOT_URL.equals(rootUrl) 81 | ? GITHUB_API_ENDPOINT_URL 82 | : String.format(GHE_API_ENDPOINT_URL, rootUrl); 83 | } 84 | 85 | private String getProtectedResourceUrl() { 86 | return getApiUrl() + "user"; 87 | } 88 | 89 | @Override 90 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 91 | OAuthRequest request = new OAuthRequest(Verb.GET, getProtectedResourceUrl()); 92 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 93 | service.signRequest(t, request); 94 | 95 | JsonElement userJson = null; 96 | try (Response response = service.execute(request)) { 97 | if (response.getCode() != HttpServletResponse.SC_OK) { 98 | throw new IOException( 99 | String.format( 100 | "Status %s (%s) for request %s", 101 | response.getCode(), response.getBody(), request.getUrl())); 102 | } 103 | userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 104 | if (log.isDebugEnabled()) { 105 | log.debug("User info response: {}", response.getBody()); 106 | } 107 | if (userJson.isJsonObject()) { 108 | JsonObject jsonObject = userJson.getAsJsonObject(); 109 | JsonElement id = jsonObject.get("id"); 110 | if (id == null || id.isJsonNull()) { 111 | throw new IOException("Response doesn't contain id field"); 112 | } 113 | JsonElement email = jsonObject.get("email"); 114 | JsonElement name = jsonObject.get("name"); 115 | JsonElement login = jsonObject.get("login"); 116 | return new OAuthUserInfo( 117 | GITHUB_PROVIDER_PREFIX + id.getAsString(), 118 | login == null || login.isJsonNull() ? null : login.getAsString(), 119 | email == null || email.isJsonNull() ? null : email.getAsString(), 120 | name == null || name.isJsonNull() ? null : name.getAsString(), 121 | fixLegacyUserId ? id.getAsString() : null); 122 | } 123 | } catch (ExecutionException | InterruptedException e) { 124 | throw new RuntimeException("Cannot retrieve user info resource", e); 125 | } 126 | 127 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 128 | } 129 | 130 | @Override 131 | public OAuthToken getAccessToken(OAuthVerifier rv) { 132 | try { 133 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 134 | return new OAuthToken( 135 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 136 | } catch (InterruptedException | ExecutionException | IOException e) { 137 | String msg = "Cannot retrieve access token"; 138 | log.error(msg, e); 139 | throw new RuntimeException(msg, e); 140 | } 141 | } 142 | 143 | @Override 144 | public String getAuthorizationUrl() { 145 | return service.getAuthorizationUrl(); 146 | } 147 | 148 | @Override 149 | public String getVersion() { 150 | return service.getVersion(); 151 | } 152 | 153 | @Override 154 | public String getName() { 155 | return "GitHub OAuth2"; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication; 19 | import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme; 20 | 21 | public class GitLabApi extends DefaultApi20 { 22 | private static final String AUTHORIZE_URL = "%s/oauth/authorize"; 23 | 24 | private final String rootUrl; 25 | 26 | public GitLabApi(String rootUrl) { 27 | this.rootUrl = rootUrl; 28 | } 29 | 30 | @Override 31 | public String getAuthorizationBaseUrl() { 32 | return String.format(AUTHORIZE_URL, rootUrl); 33 | } 34 | 35 | @Override 36 | public String getAccessTokenEndpoint() { 37 | return String.format("%s/oauth/token", rootUrl); 38 | } 39 | 40 | @Override 41 | public ClientAuthentication getClientAuthentication() { 42 | return RequestBodyAuthenticationScheme.instance(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | import static javax.servlet.http.HttpServletResponse.SC_OK; 19 | import static org.slf4j.LoggerFactory.getLogger; 20 | 21 | import com.github.scribejava.core.builder.ServiceBuilder; 22 | import com.github.scribejava.core.model.OAuth2AccessToken; 23 | import com.github.scribejava.core.model.OAuthRequest; 24 | import com.github.scribejava.core.model.Response; 25 | import com.github.scribejava.core.model.Verb; 26 | import com.github.scribejava.core.oauth.OAuth20Service; 27 | import com.google.common.base.CharMatcher; 28 | import com.google.gerrit.extensions.annotations.PluginName; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 31 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 32 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 33 | import com.google.gerrit.server.config.CanonicalWebUrl; 34 | import com.google.gerrit.server.config.PluginConfig; 35 | import com.google.gerrit.server.config.PluginConfigFactory; 36 | import com.google.gson.JsonElement; 37 | import com.google.gson.JsonObject; 38 | import com.google.inject.Inject; 39 | import com.google.inject.Provider; 40 | import com.google.inject.ProvisionException; 41 | import com.google.inject.Singleton; 42 | import java.io.IOException; 43 | import java.net.URI; 44 | import java.util.concurrent.ExecutionException; 45 | import org.slf4j.Logger; 46 | 47 | @Singleton 48 | public class GitLabOAuthService implements OAuthServiceProvider { 49 | private static final Logger log = getLogger(GitLabOAuthService.class); 50 | static final String CONFIG_SUFFIX = "-gitlab-oauth"; 51 | private static final String PROTECTED_RESOURCE_URL = "%s/api/v3/user"; 52 | private static final String GITLAB_PROVIDER_PREFIX = "gitlab-oauth:"; 53 | private final OAuth20Service service; 54 | private final String rootUrl; 55 | 56 | @Inject 57 | GitLabOAuthService( 58 | PluginConfigFactory cfgFactory, 59 | @PluginName String pluginName, 60 | @CanonicalWebUrl Provider urlProvider) { 61 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 62 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 63 | rootUrl = cfg.getString(InitOAuth.ROOT_URL); 64 | if (!URI.create(rootUrl).isAbsolute()) { 65 | throw new ProvisionException("Root URL must be absolute URL"); 66 | } 67 | service = 68 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 69 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 70 | .callback(canonicalWebUrl + "oauth") 71 | .build(new GitLabApi(rootUrl)); 72 | } 73 | 74 | @Override 75 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 76 | OAuthRequest request = 77 | new OAuthRequest(Verb.GET, String.format(PROTECTED_RESOURCE_URL, rootUrl)); 78 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 79 | service.signRequest(t, request); 80 | 81 | try (Response response = service.execute(request)) { 82 | if (response.getCode() != SC_OK) { 83 | throw new IOException( 84 | String.format( 85 | "Status %s (%s) for request %s", 86 | response.getCode(), response.getBody(), request.getUrl())); 87 | } 88 | JsonElement userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 89 | if (log.isDebugEnabled()) { 90 | log.debug("User info response: {}", response.getBody()); 91 | } 92 | JsonObject jsonObject = userJson.getAsJsonObject(); 93 | if (jsonObject == null || jsonObject.isJsonNull()) { 94 | throw new IOException("Response doesn't contain 'user' field" + jsonObject); 95 | } 96 | JsonElement id = jsonObject.get("id"); 97 | JsonElement username = jsonObject.get("username"); 98 | JsonElement email = jsonObject.get("email"); 99 | JsonElement name = jsonObject.get("name"); 100 | return new OAuthUserInfo( 101 | GITLAB_PROVIDER_PREFIX + id.getAsString(), 102 | username == null || username.isJsonNull() ? null : username.getAsString(), 103 | email == null || email.isJsonNull() ? null : email.getAsString(), 104 | name == null || name.isJsonNull() ? null : name.getAsString(), 105 | null); 106 | } catch (ExecutionException | InterruptedException e) { 107 | throw new RuntimeException("Cannot retrieve user info resource", e); 108 | } 109 | } 110 | 111 | @Override 112 | public OAuthToken getAccessToken(OAuthVerifier rv) { 113 | try { 114 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 115 | return new OAuthToken( 116 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 117 | } catch (InterruptedException | ExecutionException | IOException e) { 118 | String msg = "Cannot retrieve access token"; 119 | log.error(msg, e); 120 | throw new RuntimeException(msg, e); 121 | } 122 | } 123 | 124 | @Override 125 | public String getAuthorizationUrl() { 126 | return service.getAuthorizationUrl(); 127 | } 128 | 129 | @Override 130 | public String getVersion() { 131 | return service.getVersion(); 132 | } 133 | 134 | @Override 135 | public String getName() { 136 | return "GitLab OAuth2"; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/Google2Api.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | 19 | public class Google2Api extends DefaultApi20 { 20 | @Override 21 | public String getAccessTokenEndpoint() { 22 | return "https://www.googleapis.com/oauth2/v4/token"; 23 | } 24 | 25 | @Override 26 | public String getAuthorizationBaseUrl() { 27 | return "https://accounts.google.com/o/oauth2/auth"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | 19 | import com.github.scribejava.core.builder.ServiceBuilder; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | import com.github.scribejava.core.model.OAuthRequest; 22 | import com.github.scribejava.core.model.Response; 23 | import com.github.scribejava.core.model.Verb; 24 | import com.github.scribejava.core.oauth.OAuth20Service; 25 | import com.google.common.base.CharMatcher; 26 | import com.google.common.base.Preconditions; 27 | import com.google.common.base.Strings; 28 | import com.google.gerrit.extensions.annotations.PluginName; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 31 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 32 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 33 | import com.google.gerrit.server.config.CanonicalWebUrl; 34 | import com.google.gerrit.server.config.PluginConfig; 35 | import com.google.gerrit.server.config.PluginConfigFactory; 36 | import com.google.gson.JsonElement; 37 | import com.google.gson.JsonObject; 38 | import com.google.inject.Inject; 39 | import com.google.inject.Provider; 40 | import com.google.inject.Singleton; 41 | import java.io.IOException; 42 | import java.io.UnsupportedEncodingException; 43 | import java.net.URLEncoder; 44 | import java.nio.charset.StandardCharsets; 45 | import java.util.Arrays; 46 | import java.util.List; 47 | import java.util.concurrent.ExecutionException; 48 | import javax.servlet.http.HttpServletResponse; 49 | import org.apache.commons.codec.binary.Base64; 50 | import org.slf4j.Logger; 51 | import org.slf4j.LoggerFactory; 52 | 53 | @Singleton 54 | class GoogleOAuthService implements OAuthServiceProvider { 55 | private static final Logger log = LoggerFactory.getLogger(GoogleOAuthService.class); 56 | static final String CONFIG_SUFFIX = "-google-oauth"; 57 | private static final String GOOGLE_PROVIDER_PREFIX = "google-oauth:"; 58 | private static final String PROTECTED_RESOURCE_URL = 59 | "https://www.googleapis.com/oauth2/v2/userinfo"; 60 | private static final String SCOPE = "email profile"; 61 | private final OAuth20Service service; 62 | private final String canonicalWebUrl; 63 | private final List domains; 64 | private final boolean useEmailAsUsername; 65 | private final boolean fixLegacyUserId; 66 | 67 | @Inject 68 | GoogleOAuthService( 69 | PluginConfigFactory cfgFactory, 70 | @PluginName String pluginName, 71 | @CanonicalWebUrl Provider urlProvider) { 72 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 73 | this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 74 | if (cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_OPENID_ACCOUNT, false)) { 75 | log.warn( 76 | String.format( 77 | "The support for: %s is disconinued", InitOAuth.LINK_TO_EXISTING_OPENID_ACCOUNT)); 78 | } 79 | fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false); 80 | this.domains = Arrays.asList(cfg.getStringList(InitOAuth.DOMAIN)); 81 | this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false); 82 | this.service = 83 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 84 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 85 | .callback(canonicalWebUrl + "oauth") 86 | .defaultScope(SCOPE) 87 | .build(new Google2Api()); 88 | if (log.isDebugEnabled()) { 89 | log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl); 90 | log.debug("OAuth2: scope={}", SCOPE); 91 | log.debug("OAuth2: domains={}", domains); 92 | log.debug("OAuth2: useEmailAsUsername={}", useEmailAsUsername); 93 | } 94 | } 95 | 96 | @Override 97 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 98 | OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL); 99 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 100 | service.signRequest(t, request); 101 | 102 | JsonElement userJson = null; 103 | try (Response response = service.execute(request)) { 104 | if (response.getCode() != HttpServletResponse.SC_OK) { 105 | throw new IOException( 106 | String.format( 107 | "Status %s (%s) for request %s", 108 | response.getCode(), response.getBody(), request.getUrl())); 109 | } 110 | userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 111 | if (log.isDebugEnabled()) { 112 | log.debug("User info response: {}", response.getBody()); 113 | } 114 | if (userJson.isJsonObject()) { 115 | JsonObject jsonObject = userJson.getAsJsonObject(); 116 | JsonElement id = jsonObject.get("id"); 117 | if (id == null || id.isJsonNull()) { 118 | throw new IOException("Response doesn't contain id field"); 119 | } 120 | JsonElement email = jsonObject.get("email"); 121 | JsonElement name = jsonObject.get("name"); 122 | String login = null; 123 | 124 | if (domains.size() > 0) { 125 | boolean domainMatched = false; 126 | JsonObject jwtToken = retrieveJWTToken(token); 127 | String hdClaim = retrieveHostedDomain(jwtToken); 128 | for (String domain : domains) { 129 | if (domain.equalsIgnoreCase(hdClaim)) { 130 | domainMatched = true; 131 | break; 132 | } 133 | } 134 | if (!domainMatched) { 135 | // TODO(davido): improve error reporting in OAuth extension point 136 | log.error("Error: hosted domain validation failed: {}", Strings.nullToEmpty(hdClaim)); 137 | return null; 138 | } 139 | } 140 | if (useEmailAsUsername && !email.isJsonNull()) { 141 | login = email.getAsString().split("@")[0]; 142 | } 143 | return new OAuthUserInfo( 144 | GOOGLE_PROVIDER_PREFIX + id.getAsString() /*externalId*/, 145 | login /*username*/, 146 | email == null || email.isJsonNull() ? null : email.getAsString() /*email*/, 147 | name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/, 148 | fixLegacyUserId ? id.getAsString() : null /*claimedIdentity*/); 149 | } 150 | } catch (ExecutionException | InterruptedException e) { 151 | throw new RuntimeException("Cannot retrieve user info resource", e); 152 | } 153 | 154 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 155 | } 156 | 157 | private JsonObject retrieveJWTToken(OAuthToken token) throws IOException { 158 | JsonElement idToken = JSON.newGson().fromJson(token.getRaw(), JsonElement.class); 159 | if (idToken != null && idToken.isJsonObject()) { 160 | JsonObject idTokenObj = idToken.getAsJsonObject(); 161 | JsonElement idTokenElement = idTokenObj.get("id_token"); 162 | if (idTokenElement != null && !idTokenElement.isJsonNull()) { 163 | String payload; 164 | try { 165 | payload = decodePayload(idTokenElement.getAsString()); 166 | } catch (UnsupportedEncodingException e) { 167 | throw new IOException( 168 | String.format( 169 | "%s support is required to interact with JWTs", StandardCharsets.UTF_8.name()), 170 | e); 171 | } 172 | if (!Strings.isNullOrEmpty(payload)) { 173 | JsonElement tokenJsonElement = JSON.newGson().fromJson(payload, JsonElement.class); 174 | if (tokenJsonElement.isJsonObject()) { 175 | return tokenJsonElement.getAsJsonObject(); 176 | } 177 | } 178 | } 179 | } 180 | return null; 181 | } 182 | 183 | private static String retrieveHostedDomain(JsonObject jwtToken) { 184 | JsonElement hdClaim = jwtToken.get("hd"); 185 | if (hdClaim != null && !hdClaim.isJsonNull()) { 186 | String hd = hdClaim.getAsString(); 187 | log.debug("OAuth2: hd={}", hd); 188 | return hd; 189 | } 190 | log.debug("OAuth2: JWT doesn't contain hd element"); 191 | return null; 192 | } 193 | 194 | /** 195 | * Decode payload from JWT according to spec: "header.payload.signature" 196 | * 197 | * @param idToken Base64 encoded tripple, separated with dot 198 | * @return openid_id part of payload, when contained, null otherwise 199 | */ 200 | private static String decodePayload(String idToken) throws UnsupportedEncodingException { 201 | Preconditions.checkNotNull(idToken); 202 | String[] jwtParts = idToken.split("\\."); 203 | Preconditions.checkState(jwtParts.length == 3); 204 | String payloadStr = jwtParts[1]; 205 | Preconditions.checkNotNull(payloadStr); 206 | return new String(Base64.decodeBase64(payloadStr), StandardCharsets.UTF_8.name()); 207 | } 208 | 209 | @Override 210 | public OAuthToken getAccessToken(OAuthVerifier rv) { 211 | try { 212 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 213 | return new OAuthToken( 214 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 215 | } catch (InterruptedException | ExecutionException | IOException e) { 216 | String msg = "Cannot retrieve access token"; 217 | log.error(msg, e); 218 | throw new RuntimeException(msg, e); 219 | } 220 | } 221 | 222 | @Override 223 | public String getAuthorizationUrl() { 224 | StringBuilder urlBuilder = new StringBuilder(service.getAuthorizationUrl()); 225 | try { 226 | if (domains.size() == 1) { 227 | urlBuilder.append("&hd="); 228 | urlBuilder.append(URLEncoder.encode(domains.get(0), StandardCharsets.UTF_8.name())); 229 | } else if (domains.size() > 1) { 230 | urlBuilder.append("&hd=*"); 231 | } 232 | } catch (UnsupportedEncodingException e) { 233 | throw new IllegalArgumentException(e); 234 | } 235 | if (log.isDebugEnabled()) { 236 | log.debug("OAuth2: authorization URL={}", urlBuilder); 237 | } 238 | return urlBuilder.toString(); 239 | } 240 | 241 | @Override 242 | public String getVersion() { 243 | return service.getVersion(); 244 | } 245 | 246 | @Override 247 | public String getName() { 248 | return "Google OAuth2"; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.google.gerrit.extensions.annotations.Exports; 18 | import com.google.gerrit.extensions.annotations.PluginName; 19 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 20 | import com.google.gerrit.server.config.PluginConfig; 21 | import com.google.gerrit.server.config.PluginConfigFactory; 22 | import com.google.inject.Inject; 23 | import com.google.inject.ProvisionException; 24 | import com.google.inject.servlet.ServletModule; 25 | 26 | class HttpModule extends ServletModule { 27 | 28 | private final PluginConfigFactory cfgFactory; 29 | private final String pluginName; 30 | 31 | @Inject 32 | HttpModule(PluginConfigFactory cfgFactory, @PluginName String pluginName) { 33 | this.cfgFactory = cfgFactory; 34 | this.pluginName = pluginName; 35 | } 36 | 37 | @Override 38 | protected void configureServlets() { 39 | PluginConfig cfg = 40 | cfgFactory.getFromGerritConfig(pluginName + GoogleOAuthService.CONFIG_SUFFIX); 41 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 42 | bind(OAuthServiceProvider.class) 43 | .annotatedWith(Exports.named(GoogleOAuthService.CONFIG_SUFFIX)) 44 | .to(GoogleOAuthService.class); 45 | } 46 | 47 | cfg = cfgFactory.getFromGerritConfig(pluginName + GitHubOAuthService.CONFIG_SUFFIX); 48 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 49 | bind(OAuthServiceProvider.class) 50 | .annotatedWith(Exports.named(GitHubOAuthService.CONFIG_SUFFIX)) 51 | .to(GitHubOAuthService.class); 52 | } 53 | 54 | cfg = cfgFactory.getFromGerritConfig(pluginName + BitbucketOAuthService.CONFIG_SUFFIX); 55 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 56 | bind(OAuthServiceProvider.class) 57 | .annotatedWith(Exports.named(BitbucketOAuthService.CONFIG_SUFFIX)) 58 | .to(BitbucketOAuthService.class); 59 | } 60 | 61 | cfg = cfgFactory.getFromGerritConfig(pluginName + CasOAuthService.CONFIG_SUFFIX); 62 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 63 | bind(OAuthServiceProvider.class) 64 | .annotatedWith(Exports.named(CasOAuthService.CONFIG_SUFFIX)) 65 | .to(CasOAuthService.class); 66 | } 67 | 68 | cfg = cfgFactory.getFromGerritConfig(pluginName + FacebookOAuthService.CONFIG_SUFFIX); 69 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 70 | bind(OAuthServiceProvider.class) 71 | .annotatedWith(Exports.named(FacebookOAuthService.CONFIG_SUFFIX)) 72 | .to(FacebookOAuthService.class); 73 | } 74 | 75 | cfg = cfgFactory.getFromGerritConfig(pluginName + GitLabOAuthService.CONFIG_SUFFIX); 76 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 77 | bind(OAuthServiceProvider.class) 78 | .annotatedWith(Exports.named(GitLabOAuthService.CONFIG_SUFFIX)) 79 | .to(GitLabOAuthService.class); 80 | } 81 | 82 | cfg = cfgFactory.getFromGerritConfig(pluginName + LemonLDAPOAuthService.CONFIG_SUFFIX); 83 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 84 | bind(OAuthServiceProvider.class) 85 | .annotatedWith(Exports.named(LemonLDAPOAuthService.CONFIG_SUFFIX)) 86 | .to(LemonLDAPOAuthService.class); 87 | } 88 | 89 | cfg = cfgFactory.getFromGerritConfig(pluginName + DexOAuthService.CONFIG_SUFFIX); 90 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 91 | bind(OAuthServiceProvider.class) 92 | .annotatedWith(Exports.named(DexOAuthService.CONFIG_SUFFIX)) 93 | .to(DexOAuthService.class); 94 | } 95 | 96 | cfg = cfgFactory.getFromGerritConfig(pluginName + KeycloakOAuthService.CONFIG_SUFFIX); 97 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 98 | bind(OAuthServiceProvider.class) 99 | .annotatedWith(Exports.named(KeycloakOAuthService.CONFIG_SUFFIX)) 100 | .to(KeycloakOAuthService.class); 101 | } 102 | 103 | boolean office365LegacyProviderBound = false; 104 | cfg = 105 | cfgFactory.getFromGerritConfig( 106 | pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX_LEGACY); 107 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 108 | office365LegacyProviderBound = true; 109 | bind(OAuthServiceProvider.class) 110 | .annotatedWith(Exports.named(AzureActiveDirectoryService.CONFIG_SUFFIX)) 111 | .to(AzureActiveDirectoryService.class); 112 | } 113 | cfg = cfgFactory.getFromGerritConfig(pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX); 114 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 115 | // ?: Check if the legacy Office365 is already bound, we can only have one of these bound at 116 | // one time 117 | if (office365LegacyProviderBound) { 118 | // -> Yes, the legacy Office365 is already bound and we are trying to bind the 119 | // AzureActiveDirectoryService.CONFIG_SUFFIX at the same time. 120 | throw new ProvisionException("Legacy Office365 OAuth provider is already bound!"); 121 | } 122 | bind(OAuthServiceProvider.class) 123 | .annotatedWith(Exports.named(AzureActiveDirectoryService.CONFIG_SUFFIX)) 124 | .to(AzureActiveDirectoryService.class); 125 | } 126 | 127 | cfg = cfgFactory.getFromGerritConfig(pluginName + AirVantageOAuthService.CONFIG_SUFFIX); 128 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 129 | bind(OAuthServiceProvider.class) 130 | .annotatedWith(Exports.named(AirVantageOAuthService.CONFIG_SUFFIX)) 131 | .to(AirVantageOAuthService.class); 132 | } 133 | 134 | cfg = cfgFactory.getFromGerritConfig(pluginName + PhabricatorOAuthService.CONFIG_SUFFIX); 135 | if (cfg.getString(InitOAuth.CLIENT_ID) != null) { 136 | bind(OAuthServiceProvider.class) 137 | .annotatedWith(Exports.named(PhabricatorOAuthService.CONFIG_SUFFIX)) 138 | .to(PhabricatorOAuthService.class); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package com.googlesource.gerrit.plugins.oauth; 15 | 16 | import static java.util.Objects.requireNonNull; 17 | 18 | import com.google.common.base.Strings; 19 | import com.google.gerrit.extensions.annotations.PluginName; 20 | import com.google.gerrit.pgm.init.api.ConsoleUI; 21 | import com.google.gerrit.pgm.init.api.InitStep; 22 | import com.google.gerrit.pgm.init.api.Section; 23 | import com.google.inject.Inject; 24 | import com.google.inject.ProvisionException; 25 | import java.net.URI; 26 | 27 | class InitOAuth implements InitStep { 28 | static final String PLUGIN_SECTION = "plugin"; 29 | static final String CLIENT_ID = "client-id"; 30 | static final String CLIENT_SECRET = "client-secret"; 31 | static final String LINK_TO_EXISTING_OPENID_ACCOUNT = "link-to-existing-openid-accounts"; 32 | static final String FIX_LEGACY_USER_ID = "fix-legacy-user-id"; 33 | static final String DOMAIN = "domain"; 34 | static final String USE_EMAIL_AS_USERNAME = "use-email-as-username"; 35 | static final String USE_PREFERRED_USERNAME = "use-preferred-username"; 36 | static final String ROOT_URL = "root-url"; 37 | static final String REALM = "realm"; 38 | static final String TENANT = "tenant"; 39 | static final String LINK_TO_EXISTING_OFFICE365_ACCOUNT = "link-to-existing-office365-accounts"; 40 | static final String SERVICE_NAME = "service-name"; 41 | static String FIX_LEGACY_USER_ID_QUESTION = "Fix legacy user id, without oauth provider prefix?"; 42 | 43 | private final ConsoleUI ui; 44 | private final Section googleOAuthProviderSection; 45 | private final Section githubOAuthProviderSection; 46 | private final Section bitbucketOAuthProviderSection; 47 | private final Section casOAuthProviderSection; 48 | private final Section facebookOAuthProviderSection; 49 | private final Section gitlabOAuthProviderSection; 50 | private final Section lemonldapOAuthProviderSection; 51 | private final Section dexOAuthProviderSection; 52 | private final Section keycloakOAuthProviderSection; 53 | private final Section office365OAuthProviderSection; 54 | private final Section azureActiveDirectoryAuthProviderSection; 55 | private final Section airVantageOAuthProviderSection; 56 | private final Section phabricatorOAuthProviderSection; 57 | 58 | @Inject 59 | InitOAuth(ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) { 60 | this.ui = ui; 61 | this.googleOAuthProviderSection = 62 | sections.get(PLUGIN_SECTION, pluginName + GoogleOAuthService.CONFIG_SUFFIX); 63 | this.githubOAuthProviderSection = 64 | sections.get(PLUGIN_SECTION, pluginName + GitHubOAuthService.CONFIG_SUFFIX); 65 | this.bitbucketOAuthProviderSection = 66 | sections.get(PLUGIN_SECTION, pluginName + BitbucketOAuthService.CONFIG_SUFFIX); 67 | this.casOAuthProviderSection = 68 | sections.get(PLUGIN_SECTION, pluginName + CasOAuthService.CONFIG_SUFFIX); 69 | this.facebookOAuthProviderSection = 70 | sections.get(PLUGIN_SECTION, pluginName + FacebookOAuthService.CONFIG_SUFFIX); 71 | this.gitlabOAuthProviderSection = 72 | sections.get(PLUGIN_SECTION, pluginName + GitLabOAuthService.CONFIG_SUFFIX); 73 | this.lemonldapOAuthProviderSection = 74 | sections.get(PLUGIN_SECTION, pluginName + LemonLDAPOAuthService.CONFIG_SUFFIX); 75 | this.dexOAuthProviderSection = 76 | sections.get(PLUGIN_SECTION, pluginName + DexOAuthService.CONFIG_SUFFIX); 77 | this.keycloakOAuthProviderSection = 78 | sections.get(PLUGIN_SECTION, pluginName + KeycloakOAuthService.CONFIG_SUFFIX); 79 | this.office365OAuthProviderSection = 80 | sections.get(PLUGIN_SECTION, pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX_LEGACY); 81 | this.azureActiveDirectoryAuthProviderSection = 82 | sections.get(PLUGIN_SECTION, pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX); 83 | this.airVantageOAuthProviderSection = 84 | sections.get(PLUGIN_SECTION, pluginName + AirVantageOAuthService.CONFIG_SUFFIX); 85 | this.phabricatorOAuthProviderSection = 86 | sections.get(PLUGIN_SECTION, pluginName + PhabricatorOAuthService.CONFIG_SUFFIX); 87 | } 88 | 89 | @Override 90 | public void run() throws Exception { 91 | ui.header("OAuth Authentication Provider"); 92 | 93 | boolean configureGoogleOAuthProvider = 94 | ui.yesno( 95 | isConfigured(googleOAuthProviderSection), 96 | "Use Google OAuth provider for Gerrit login ?"); 97 | if (configureGoogleOAuthProvider && configureOAuth(googleOAuthProviderSection)) { 98 | googleOAuthProviderSection.string(FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false"); 99 | } 100 | 101 | boolean configueGitHubOAuthProvider = 102 | ui.yesno( 103 | isConfigured(githubOAuthProviderSection), 104 | "Use GitHub OAuth provider for Gerrit login ?"); 105 | if (configueGitHubOAuthProvider && configureOAuth(githubOAuthProviderSection)) { 106 | githubOAuthProviderSection.string(FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false"); 107 | } 108 | 109 | boolean configureBitbucketOAuthProvider = 110 | ui.yesno( 111 | isConfigured(bitbucketOAuthProviderSection), 112 | "Use Bitbucket OAuth provider for Gerrit login ?"); 113 | if (configureBitbucketOAuthProvider && configureOAuth(bitbucketOAuthProviderSection)) { 114 | bitbucketOAuthProviderSection.string( 115 | FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false"); 116 | } 117 | 118 | boolean configureCasOAuthProvider = 119 | ui.yesno( 120 | isConfigured(casOAuthProviderSection), "Use CAS OAuth provider for Gerrit login ?"); 121 | if (configureCasOAuthProvider && configureOAuth(casOAuthProviderSection)) { 122 | checkRootUrl(casOAuthProviderSection.string("CAS Root URL", ROOT_URL, null)); 123 | casOAuthProviderSection.string(FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false"); 124 | } 125 | 126 | boolean configueFacebookOAuthProvider = 127 | ui.yesno( 128 | isConfigured(facebookOAuthProviderSection), 129 | "Use Facebook OAuth provider for Gerrit login ?"); 130 | if (configueFacebookOAuthProvider) { 131 | configureOAuth(facebookOAuthProviderSection); 132 | } 133 | 134 | boolean configureGitLabOAuthProvider = 135 | ui.yesno( 136 | isConfigured(gitlabOAuthProviderSection), 137 | "Use GitLab OAuth provider for Gerrit login ?"); 138 | if (configureGitLabOAuthProvider && configureOAuth(gitlabOAuthProviderSection)) { 139 | checkRootUrl(gitlabOAuthProviderSection.string("GitLab Root URL", ROOT_URL, null)); 140 | } 141 | 142 | boolean configureLemonLDAPOAuthProvider = 143 | ui.yesno( 144 | isConfigured(lemonldapOAuthProviderSection), 145 | "Use LemonLDAP OAuth provider for Gerrit login ?"); 146 | if (configureLemonLDAPOAuthProvider) { 147 | checkRootUrl(lemonldapOAuthProviderSection.string("LemonLDAP Root URL", ROOT_URL, null)); 148 | configureOAuth(lemonldapOAuthProviderSection); 149 | } 150 | 151 | boolean configureDexOAuthProvider = 152 | ui.yesno( 153 | isConfigured(dexOAuthProviderSection), "Use Dex OAuth provider for Gerrit login ?"); 154 | if (configureDexOAuthProvider && configureOAuth(dexOAuthProviderSection)) { 155 | checkRootUrl(dexOAuthProviderSection.string("Dex Root URL", ROOT_URL, null)); 156 | } 157 | 158 | boolean configureKeycloakOAuthProvider = 159 | ui.yesno( 160 | isConfigured(keycloakOAuthProviderSection), 161 | "Use Keycloak OAuth provider for Gerrit login ?"); 162 | if (configureKeycloakOAuthProvider && configureOAuth(keycloakOAuthProviderSection)) { 163 | checkRootUrl(keycloakOAuthProviderSection.string("Keycloak Root URL", ROOT_URL, null)); 164 | keycloakOAuthProviderSection.string("Keycloak Realm", REALM, null); 165 | } 166 | 167 | // ?: Are there legacy office365 already configured on the system? 168 | if (isConfigured(office365OAuthProviderSection)) { 169 | // -> Yes, this system has already configured the old legacy office365. 170 | boolean configureOffice365OAuthProvider = 171 | ui.yesno( 172 | isConfigured(office365OAuthProviderSection), 173 | "Use Office365 OAuth provider for Gerrit login ?"); 174 | if (configureOffice365OAuthProvider) { 175 | configureOAuth(office365OAuthProviderSection); 176 | } 177 | } 178 | // E-> No, we either are setting up on an new system or using the new azure config 179 | else { 180 | boolean configureAzureActiveDirectoryAuthProvider = 181 | ui.yesno( 182 | isConfigured(azureActiveDirectoryAuthProviderSection), 183 | "Use Azure OAuth provider for Gerrit login ?"); 184 | if (configureAzureActiveDirectoryAuthProvider) { 185 | configureOAuth(azureActiveDirectoryAuthProviderSection); 186 | azureActiveDirectoryAuthProviderSection.string( 187 | "Tenant", TENANT, AzureActiveDirectoryService.DEFAULT_TENANT); 188 | } 189 | } 190 | 191 | boolean configureAirVantageOAuthProvider = 192 | ui.yesno( 193 | isConfigured(airVantageOAuthProviderSection), 194 | "Use AirVantage OAuth provider for Gerrit login ?"); 195 | if (configureAirVantageOAuthProvider) { 196 | configureOAuth(airVantageOAuthProviderSection); 197 | } 198 | 199 | boolean configurePhabricatorOAuthProvider = 200 | ui.yesno( 201 | isConfigured(phabricatorOAuthProviderSection), 202 | "Use Phabricator OAuth provider for Gerrit login ?"); 203 | if (configurePhabricatorOAuthProvider && configureOAuth(phabricatorOAuthProviderSection)) { 204 | checkRootUrl(phabricatorOAuthProviderSection.string("Phabricator Root URL", ROOT_URL, null)); 205 | } 206 | } 207 | 208 | /** 209 | * Retrieve client id to check whether or not this provider was already configured. 210 | * 211 | * @param s OAuth provider section 212 | * @return true if client id key is present, false otherwise 213 | */ 214 | private static boolean isConfigured(Section s) { 215 | return !Strings.isNullOrEmpty(s.get(CLIENT_ID)); 216 | } 217 | 218 | /** 219 | * Configure OAuth provider section 220 | * 221 | * @param s section to configure 222 | * @return true if section is present, false otherwise 223 | */ 224 | private static boolean configureOAuth(Section s) { 225 | if (!Strings.isNullOrEmpty(s.string("Application client id", CLIENT_ID, null))) { 226 | s.passwordForKey("Application client secret", CLIENT_SECRET); 227 | return true; 228 | } 229 | return false; 230 | } 231 | 232 | /** 233 | * Check root URL parameter. It must be not null and it must be an absolute URI. 234 | * 235 | * @param rootUrl root URL 236 | * @throws ProvisionException if rootUrl wasn't provided or is not absolute URI. 237 | */ 238 | private static void checkRootUrl(String rootUrl) { 239 | requireNonNull(rootUrl); 240 | if (!URI.create(rootUrl).isAbsolute()) { 241 | throw new ProvisionException("Root URL must be absolute URL"); 242 | } 243 | } 244 | 245 | @Override 246 | public void postRun() throws Exception {} 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; 19 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; 20 | import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication; 21 | import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme; 22 | 23 | public class KeycloakApi extends DefaultApi20 { 24 | 25 | private static final String AUTHORIZE_URL = "%s/auth/realms/%s/protocol/openid-connect/auth"; 26 | 27 | private final String rootUrl; 28 | private final String realm; 29 | 30 | public KeycloakApi(String rootUrl, String realm) { 31 | this.rootUrl = rootUrl; 32 | this.realm = realm; 33 | } 34 | 35 | @Override 36 | public String getAuthorizationBaseUrl() { 37 | return String.format(AUTHORIZE_URL, rootUrl, realm); 38 | } 39 | 40 | @Override 41 | public String getAccessTokenEndpoint() { 42 | return String.format("%s/auth/realms/%s/protocol/openid-connect/token", rootUrl, realm); 43 | } 44 | 45 | @Override 46 | public BearerSignature getBearerSignature() { 47 | return BearerSignatureURIQueryParameter.instance(); 48 | } 49 | 50 | @Override 51 | public ClientAuthentication getClientAuthentication() { 52 | return RequestBodyAuthenticationScheme.instance(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | 19 | import com.github.scribejava.core.builder.ServiceBuilder; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | import com.github.scribejava.core.oauth.OAuth20Service; 22 | import com.google.common.base.CharMatcher; 23 | import com.google.common.base.Preconditions; 24 | import com.google.gerrit.extensions.annotations.PluginName; 25 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 26 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 27 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 28 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 29 | import com.google.gerrit.server.config.CanonicalWebUrl; 30 | import com.google.gerrit.server.config.PluginConfig; 31 | import com.google.gerrit.server.config.PluginConfigFactory; 32 | import com.google.gson.JsonElement; 33 | import com.google.gson.JsonObject; 34 | import com.google.inject.Inject; 35 | import com.google.inject.Provider; 36 | import com.google.inject.ProvisionException; 37 | import java.io.IOException; 38 | import java.io.UnsupportedEncodingException; 39 | import java.net.URI; 40 | import java.nio.charset.StandardCharsets; 41 | import java.util.concurrent.ExecutionException; 42 | import org.apache.commons.codec.binary.Base64; 43 | import org.slf4j.Logger; 44 | import org.slf4j.LoggerFactory; 45 | 46 | public class KeycloakOAuthService implements OAuthServiceProvider { 47 | 48 | private static final Logger log = LoggerFactory.getLogger(KeycloakOAuthService.class); 49 | 50 | static final String CONFIG_SUFFIX = "-keycloak-oauth"; 51 | private static final String KEYCLOAK_PROVIDER_PREFIX = "keycloak-oauth:"; 52 | private final OAuth20Service service; 53 | private final String serviceName; 54 | private final boolean usePreferredUsername; 55 | 56 | @Inject 57 | KeycloakOAuthService( 58 | PluginConfigFactory cfgFactory, 59 | @PluginName String pluginName, 60 | @CanonicalWebUrl Provider urlProvider) { 61 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 62 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 63 | 64 | String rootUrl = cfg.getString(InitOAuth.ROOT_URL); 65 | if (!URI.create(rootUrl).isAbsolute()) { 66 | throw new ProvisionException("Root URL must be absolute URL"); 67 | } 68 | String realm = cfg.getString(InitOAuth.REALM); 69 | serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Keycloak OAuth2"); 70 | usePreferredUsername = cfg.getBoolean(InitOAuth.USE_PREFERRED_USERNAME, true); 71 | 72 | service = 73 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 74 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 75 | .callback(canonicalWebUrl + "oauth") 76 | .defaultScope("openid") 77 | .build(new KeycloakApi(rootUrl, realm)); 78 | } 79 | 80 | private String parseJwt(String input) throws UnsupportedEncodingException { 81 | String[] parts = input.split("\\."); 82 | Preconditions.checkState(parts.length == 3); 83 | Preconditions.checkNotNull(parts[1]); 84 | return new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8.name()); 85 | } 86 | 87 | @Override 88 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 89 | JsonElement tokenJson = JSON.newGson().fromJson(token.getRaw(), JsonElement.class); 90 | JsonObject tokenObject = tokenJson.getAsJsonObject(); 91 | JsonElement id_token = tokenObject.get("id_token"); 92 | String jwt; 93 | try { 94 | jwt = parseJwt(id_token.getAsString()); 95 | } catch (UnsupportedEncodingException e) { 96 | throw new IOException( 97 | String.format( 98 | "%s support is required to interact with JWTs", StandardCharsets.UTF_8.name()), 99 | e); 100 | } 101 | 102 | JsonElement claimJson = JSON.newGson().fromJson(jwt, JsonElement.class); 103 | 104 | JsonObject claimObject = claimJson.getAsJsonObject(); 105 | if (log.isDebugEnabled()) { 106 | log.debug("Claim object: {}", claimObject); 107 | } 108 | JsonElement usernameElement = claimObject.get("preferred_username"); 109 | JsonElement emailElement = claimObject.get("email"); 110 | JsonElement nameElement = claimObject.get("name"); 111 | if (usernameElement == null || usernameElement.isJsonNull()) { 112 | throw new IOException("Response doesn't contain preferred_username field"); 113 | } 114 | if (emailElement == null || emailElement.isJsonNull()) { 115 | throw new IOException("Response doesn't contain email field"); 116 | } 117 | if (nameElement == null || nameElement.isJsonNull()) { 118 | throw new IOException("Response doesn't contain name field"); 119 | } 120 | String usernameAsString = usernameElement.getAsString(); 121 | String username = null; 122 | if (usePreferredUsername) { 123 | username = usernameAsString; 124 | } 125 | String externalId = KEYCLOAK_PROVIDER_PREFIX + usernameAsString; 126 | String email = emailElement.getAsString(); 127 | String name = nameElement.getAsString(); 128 | 129 | return new OAuthUserInfo( 130 | externalId /*externalId*/, 131 | username /*username*/, 132 | email /*email*/, 133 | name /*displayName*/, 134 | null /*claimedIdentity*/); 135 | } 136 | 137 | @Override 138 | public OAuthToken getAccessToken(OAuthVerifier rv) { 139 | try { 140 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 141 | return new OAuthToken( 142 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 143 | } catch (InterruptedException | ExecutionException | IOException e) { 144 | String msg = "Cannot retrieve access token"; 145 | log.error(msg, e); 146 | throw new RuntimeException(msg, e); 147 | } 148 | } 149 | 150 | @Override 151 | public String getAuthorizationUrl() { 152 | return service.getAuthorizationUrl(); 153 | } 154 | 155 | @Override 156 | public String getVersion() { 157 | return service.getVersion(); 158 | } 159 | 160 | @Override 161 | public String getName() { 162 | return serviceName; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication; 19 | import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme; 20 | 21 | public class LemonLDAPApi extends DefaultApi20 { 22 | private static final String AUTHORIZE_URL = "%s/oauth2/authorize"; 23 | 24 | private final String rootUrl; 25 | 26 | public LemonLDAPApi(String rootUrl) { 27 | this.rootUrl = rootUrl; 28 | } 29 | 30 | @Override 31 | public String getAuthorizationBaseUrl() { 32 | return String.format(AUTHORIZE_URL, rootUrl); 33 | } 34 | 35 | @Override 36 | public String getAccessTokenEndpoint() { 37 | return String.format("%s/oauth2/token", rootUrl); 38 | } 39 | 40 | // TODO(davido): Remove this override, if HttpBasicAuthentication 41 | // scheme is supported. 42 | @Override 43 | public ClientAuthentication getClientAuthentication() { 44 | return RequestBodyAuthenticationScheme.instance(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | import static org.slf4j.LoggerFactory.getLogger; 19 | 20 | import com.github.scribejava.core.builder.ServiceBuilder; 21 | import com.github.scribejava.core.model.OAuth2AccessToken; 22 | import com.github.scribejava.core.model.OAuthRequest; 23 | import com.github.scribejava.core.model.Response; 24 | import com.github.scribejava.core.model.Verb; 25 | import com.github.scribejava.core.oauth.OAuth20Service; 26 | import com.google.common.base.CharMatcher; 27 | import com.google.gerrit.extensions.annotations.PluginName; 28 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 31 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 32 | import com.google.gerrit.server.config.CanonicalWebUrl; 33 | import com.google.gerrit.server.config.PluginConfig; 34 | import com.google.gerrit.server.config.PluginConfigFactory; 35 | import com.google.gson.JsonElement; 36 | import com.google.gson.JsonObject; 37 | import com.google.inject.Inject; 38 | import com.google.inject.Provider; 39 | import com.google.inject.Singleton; 40 | import java.io.IOException; 41 | import java.util.concurrent.ExecutionException; 42 | import javax.servlet.http.HttpServletResponse; 43 | import org.slf4j.Logger; 44 | 45 | @Singleton 46 | public class LemonLDAPOAuthService implements OAuthServiceProvider { 47 | private static final Logger log = getLogger(LemonLDAPOAuthService.class); 48 | static final String CONFIG_SUFFIX = "-lemonldap-oauth"; 49 | private static final String PROTECTED_RESOURCE_URL = "%s/oauth2/userinfo"; 50 | private static final String LEMONLDAP_PROVIDER_PREFIX = "llng-oauth:"; 51 | private final OAuth20Service service; 52 | private final String rootUrl; 53 | 54 | @Inject 55 | LemonLDAPOAuthService( 56 | PluginConfigFactory cfgFactory, 57 | @PluginName String pluginName, 58 | @CanonicalWebUrl Provider urlProvider) { 59 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 60 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 61 | rootUrl = cfg.getString(InitOAuth.ROOT_URL); 62 | service = 63 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 64 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 65 | .defaultScope("openid profile email") 66 | .callback(canonicalWebUrl + "oauth") 67 | .build(new LemonLDAPApi(rootUrl)); 68 | } 69 | 70 | @Override 71 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 72 | OAuthRequest request = 73 | new OAuthRequest(Verb.GET, String.format(PROTECTED_RESOURCE_URL, rootUrl)); 74 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 75 | service.signRequest(t, request); 76 | 77 | try (Response response = service.execute(request)) { 78 | if (response.getCode() != HttpServletResponse.SC_OK) { 79 | throw new IOException( 80 | String.format( 81 | "Status %s (%s) for request %s", 82 | response.getCode(), response.getBody(), request.getUrl())); 83 | } 84 | JsonElement userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 85 | if (log.isDebugEnabled()) { 86 | log.debug("User info response: {}", response.getBody()); 87 | } 88 | JsonObject jsonObject = userJson.getAsJsonObject(); 89 | if (jsonObject == null || jsonObject.isJsonNull()) { 90 | throw new IOException("Response doesn't contain 'user' field" + jsonObject); 91 | } 92 | JsonElement id = jsonObject.get("sub"); 93 | JsonElement username = jsonObject.get("preferred_username"); 94 | JsonElement email = jsonObject.get("email"); 95 | JsonElement name = jsonObject.get("name"); 96 | return new OAuthUserInfo( 97 | LEMONLDAP_PROVIDER_PREFIX + id.getAsString(), 98 | username == null || username.isJsonNull() ? null : username.getAsString(), 99 | email == null || email.isJsonNull() ? null : email.getAsString(), 100 | name == null || name.isJsonNull() ? null : name.getAsString(), 101 | null); 102 | } catch (ExecutionException | InterruptedException e) { 103 | throw new RuntimeException("Cannot retrieve user info resource", e); 104 | } 105 | } 106 | 107 | @Override 108 | public OAuthToken getAccessToken(OAuthVerifier rv) { 109 | try { 110 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 111 | return new OAuthToken( 112 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 113 | } catch (InterruptedException | ExecutionException | IOException e) { 114 | String msg = "Cannot retrieve access token"; 115 | log.error(msg, e); 116 | throw new RuntimeException(msg, e); 117 | } 118 | } 119 | 120 | @Override 121 | public String getAuthorizationUrl() { 122 | return service.getAuthorizationUrl(); 123 | } 124 | 125 | @Override 126 | public String getVersion() { 127 | return service.getVersion(); 128 | } 129 | 130 | @Override 131 | public String getName() { 132 | return "LemonLDAP::NG OAuth2 provider"; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.google.gerrit.extensions.annotations.Exports; 18 | import com.google.gerrit.extensions.annotations.PluginName; 19 | import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider; 20 | import com.google.inject.AbstractModule; 21 | import com.google.inject.Inject; 22 | 23 | public class Module extends AbstractModule { 24 | private final String pluginName; 25 | 26 | @Inject 27 | Module(@PluginName String pluginName) { 28 | this.pluginName = pluginName; 29 | } 30 | 31 | @Override 32 | protected void configure() { 33 | bind(OAuthLoginProvider.class) 34 | .annotatedWith(Exports.named(pluginName)) 35 | .to(DisabledOAuthLoginProvider.class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApi.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import com.github.scribejava.core.builder.api.DefaultApi20; 18 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; 19 | import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; 20 | 21 | public class PhabricatorApi extends DefaultApi20 { 22 | private static final String AUTHORIZE_URL = "%s/oauthserver/auth/"; 23 | 24 | private final String rootUrl; 25 | 26 | public PhabricatorApi(String rootUrl) { 27 | this.rootUrl = rootUrl; 28 | } 29 | 30 | @Override 31 | public String getAuthorizationBaseUrl() { 32 | return String.format(AUTHORIZE_URL, rootUrl); 33 | } 34 | 35 | @Override 36 | public String getAccessTokenEndpoint() { 37 | return String.format("%s/oauthserver/token/", rootUrl); 38 | } 39 | 40 | @Override 41 | public BearerSignature getBearerSignature() { 42 | return BearerSignatureURIQueryParameter.instance(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorOAuthService.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.gerrit.json.OutputFormat.JSON; 18 | 19 | import com.github.scribejava.core.builder.ServiceBuilder; 20 | import com.github.scribejava.core.model.OAuth2AccessToken; 21 | import com.github.scribejava.core.model.OAuthRequest; 22 | import com.github.scribejava.core.model.Response; 23 | import com.github.scribejava.core.model.Verb; 24 | import com.github.scribejava.core.oauth.OAuth20Service; 25 | import com.google.common.base.CharMatcher; 26 | import com.google.gerrit.extensions.annotations.PluginName; 27 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 28 | import com.google.gerrit.extensions.auth.oauth.OAuthToken; 29 | import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 30 | import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 31 | import com.google.gerrit.server.config.CanonicalWebUrl; 32 | import com.google.gerrit.server.config.PluginConfig; 33 | import com.google.gerrit.server.config.PluginConfigFactory; 34 | import com.google.gson.JsonElement; 35 | import com.google.gson.JsonObject; 36 | import com.google.inject.Inject; 37 | import com.google.inject.Provider; 38 | import com.google.inject.ProvisionException; 39 | import com.google.inject.Singleton; 40 | import java.io.IOException; 41 | import java.net.URI; 42 | import java.util.concurrent.ExecutionException; 43 | import javax.servlet.http.HttpServletResponse; 44 | import org.slf4j.Logger; 45 | import org.slf4j.LoggerFactory; 46 | 47 | @Singleton 48 | class PhabricatorOAuthService implements OAuthServiceProvider { 49 | private static final Logger log = LoggerFactory.getLogger(PhabricatorOAuthService.class); 50 | static final String CONFIG_SUFFIX = "-phabricator-oauth"; 51 | private static final String PHABRICATOR_PROVIDER_PREFIX = "phabricator-oauth:"; 52 | private static final String PROTECTED_RESOURCE_URL = "%s/api/user.whoami"; 53 | private final String rootUrl; 54 | private final OAuth20Service service; 55 | 56 | @Inject 57 | PhabricatorOAuthService( 58 | PluginConfigFactory cfgFactory, 59 | @PluginName String pluginName, 60 | @CanonicalWebUrl Provider urlProvider) { 61 | PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); 62 | String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; 63 | rootUrl = cfg.getString(InitOAuth.ROOT_URL); 64 | if (!URI.create(rootUrl).isAbsolute()) { 65 | throw new ProvisionException("Root URL must be absolute URL"); 66 | } 67 | this.service = 68 | new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) 69 | .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) 70 | .callback(canonicalWebUrl + "oauth") 71 | .build(new PhabricatorApi(rootUrl)); 72 | if (log.isDebugEnabled()) { 73 | log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl); 74 | } 75 | } 76 | 77 | @Override 78 | public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { 79 | OAuthRequest request = 80 | new OAuthRequest(Verb.GET, String.format(PROTECTED_RESOURCE_URL, rootUrl)); 81 | OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); 82 | service.signRequest(t, request); 83 | 84 | JsonElement userJson = null; 85 | try (Response response = service.execute(request)) { 86 | if (response.getCode() != HttpServletResponse.SC_OK) { 87 | throw new IOException( 88 | String.format( 89 | "Status %s (%s) for request %s", 90 | response.getCode(), response.getBody(), request.getUrl())); 91 | } 92 | userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); 93 | if (log.isDebugEnabled()) { 94 | log.debug("User info response: {}", response.getBody()); 95 | } 96 | if (userJson.isJsonObject()) { 97 | JsonObject jsonObject = userJson.getAsJsonObject(); 98 | JsonElement jsonResult = jsonObject.get("result"); 99 | if (jsonResult == null) { 100 | throw new IOException("Response doesn't contain result field"); 101 | } 102 | JsonObject resultObject = jsonResult.getAsJsonObject(); 103 | JsonElement id = resultObject.get("phid"); 104 | if (id == null || id.isJsonNull()) { 105 | throw new IOException("Response doesn't contain id field"); 106 | } 107 | JsonElement email = resultObject.get("primaryEmail"); 108 | JsonElement name = resultObject.get("realName"); 109 | JsonElement username = resultObject.get("userName"); 110 | String login = null; 111 | 112 | if (!username.isJsonNull()) { 113 | login = username.getAsString(); 114 | } 115 | return new OAuthUserInfo( 116 | PHABRICATOR_PROVIDER_PREFIX + id.getAsString() /*externalId*/, 117 | login /*username*/, 118 | email == null || email.isJsonNull() ? null : email.getAsString() /*email*/, 119 | name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/, 120 | null); 121 | } 122 | } catch (ExecutionException | InterruptedException e) { 123 | throw new RuntimeException("Cannot retrieve user info resource", e); 124 | } 125 | 126 | throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); 127 | } 128 | 129 | @Override 130 | public OAuthToken getAccessToken(OAuthVerifier rv) { 131 | try { 132 | OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); 133 | return new OAuthToken( 134 | accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); 135 | } catch (InterruptedException | ExecutionException | IOException e) { 136 | String msg = "Cannot retrieve access token"; 137 | log.error(msg, e); 138 | throw new RuntimeException(msg, e); 139 | } 140 | } 141 | 142 | @Override 143 | public String getAuthorizationUrl() { 144 | return service.getAuthorizationUrl(); 145 | } 146 | 147 | @Override 148 | public String getVersion() { 149 | return service.getVersion(); 150 | } 151 | 152 | @Override 153 | public String getName() { 154 | return "Phabricator OAuth2"; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/resources/Documentation/build.md: -------------------------------------------------------------------------------- 1 | Build 2 | ===== 3 | 4 | This plugin is built with Bazel. To install Bazel, follow 5 | the instruction on: https://www.bazel.io/versions/master/docs/install.html. 6 | 7 | Two build modes are supported: Standalone and in Gerrit tree. 8 | The standalone build mode is recommended, as this mode doesn't 9 | require the Gerrit tree to exist locally. 10 | 11 | ### Build standalone 12 | 13 | Clone the plugin: 14 | 15 | ``` 16 | git clone https://github.com/davido/gerrit-oauth-provider 17 | cd gerrit-oauth-provider 18 | ``` 19 | 20 | Issue the command: 21 | 22 | ``` 23 | bazel build :all 24 | ``` 25 | 26 | The output is created in 27 | 28 | ``` 29 | bazel-bin/@PLUGIN@.jar 30 | ``` 31 | 32 | This project can be imported into the Eclipse IDE: 33 | 34 | ``` 35 | ./tools/eclipse/project.py 36 | ``` 37 | 38 | ### Build in Gerrit tree 39 | 40 | Clone or link this plugin to the plugins directory of Gerrit's 41 | source tree, and issue the command: 42 | 43 | ``` 44 | git clone https://gerrit.googlesource.com/gerrit 45 | git clone https://gerrit.googlesource.com/plugins/@PLUGIN@ 46 | cd gerrit/plugins 47 | ln -s ../../@PLUGIN@ . 48 | ``` 49 | 50 | Put the external dependency Bazel build file into the Gerrit /plugins 51 | directory, replacing the existing empty one. 52 | 53 | ``` 54 | cd gerrit/plugins 55 | rm external_plugin_deps.bzl 56 | ln -s @PLUGIN@/external_plugin_deps.bzl . 57 | ``` 58 | 59 | From Gerrit source tree issue the command: 60 | 61 | ``` 62 | bazel build plugins/@PLUGIN@ 63 | ``` 64 | 65 | The output is created in 66 | 67 | ``` 68 | bazel-bin/plugins/@PLUGIN@/@PLUGIN@.jar 69 | ``` 70 | 71 | To execute the tests run either one of: 72 | 73 | ``` 74 | bazel test --test_tag_filters=@PLUGIN@ //... 75 | bazel test plugins/@PLUGIN@:@PLUGIN@_tests 76 | ``` 77 | 78 | This project can be imported into the Eclipse IDE. 79 | Add the plugin name to the `CUSTOM_PLUGINS` set in 80 | Gerrit core in `tools/bzl/plugins.bzl`, and execute: 81 | 82 | ``` 83 | ./tools/eclipse/project.py 84 | ``` 85 | 86 | [Back to @PLUGIN@ documentation index][index] 87 | 88 | [index]: index.html 89 | -------------------------------------------------------------------------------- /src/main/resources/Documentation/config.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | The configuration of the @PLUGIN@ plugin is done in the `gerrit.config` 5 | file. `auth.type` must be set to `OAUTH`: 6 | 7 | ``` 8 | [auth] 9 | type = OAUTH 10 | ``` 11 | 12 | Providers are configured under @PLUGIN@ section, 13 | appended with provider suffix: e.g. `-google-oauth` or `-github-oauth`: 14 | 15 | ``` 16 | [plugin "@PLUGIN@-google-oauth"] 17 | client-id = "" 18 | client-secret = "" 19 | link-to-existing-openid-accounts = true 20 | 21 | [plugin "@PLUGIN@-github-oauth"] 22 | root-url = "" # https://github.com/ or https://git.company.com/ 23 | client-id = "" 24 | client-secret = "" 25 | 26 | [plugin "@PLUGIN@-cas-oauth"] 27 | root-url = "" 28 | client-id = "" 29 | client-secret = "" 30 | 31 | [plugin "@PLUGIN@-gitlab-oauth"] 32 | root-url = "" 33 | client-id = "" 34 | client-secret = "" 35 | 36 | [plugin "@PLUGIN@-dex-oauth"] 37 | domain = "" 38 | service-name = "" 39 | root-url = "" 40 | client-id = "" 41 | client-secret = "" 42 | 43 | [plugin "@PLUGIN@-airvantage-oauth"] 44 | client-id = "" 45 | client-secret = "" 46 | 47 | [plugin "@PLUGIN@-phabricator-oauth"] 48 | client-id = "" 49 | client-secret = "" 50 | root-url = "" 51 | 52 | # The office365 has been renamed to azure and is deprecated. 53 | [plugin "@PLUGIN@-office365-oauth"] 54 | client-id = "" 55 | client-secret = "" 56 | tenant = "" 57 | 58 | [plugin "@PLUGIN@-azure-oauth"] 59 | client-id = "" 60 | client-secret = "" 61 | tenant = "" 62 | link-to-existing-office365-accounts = true #Optional, if set will try to link old account with the @PLUGIN@-office365-oauth naming 63 | 64 | [plugin "@PLUGIN@-keycloak-oauth"] 65 | root-url = "" # for example, https://signon.example.com 66 | realm = "" 67 | client-id = "" 68 | client-secret = "" 69 | use-preferred-username = true # Optional, if false will not send preferred_username from Keycloak to leave username unset 70 | 71 | ``` 72 | 73 | When one from the sections above is omitted, OAuth SSO is used. 74 | The login form with provider selection isn’t shown. When all 75 | sections are omitted, Gerrit will not start. 76 | 77 | Google OAuth provider seamlessly supports linking of OAuth identity 78 | to existing OpenID accounts. This feature is deactivated by default. 79 | To activate it, add 80 | 81 | ``` 82 | plugin.gerrit-oauth-provider-google-oauth.link-to-existing-openid-accounts = true 83 | ``` 84 | 85 | to Google OAuth configuration section. 86 | 87 | It is possile to restrict sign-in to accounts of one or more (hosted) domains for 88 | Google OAuth. Multiple `domain` options can be added: 89 | 90 | ``` 91 | plugin.gerrit-oauth-provider-google-oauth.domain = "mycollege.edu" 92 | plugin.gerrit-oauth-provider-google-oauth.domain = "myschool.net" 93 | ``` 94 | 95 | (See the spec)[https://developers.google.com/identity/protocols/OpenIDConnect#hd-param] 96 | for more information. To protect against client-side request modification, the returned 97 | ID token is checked to contain a matching hd claim (which is proof the account does belong 98 | to the hosted domain). If the hd claim wasn't included in ID token or didn't match the 99 | provided `domain` configuration option the authentication is rejected. Note: Because of 100 | current limitation of the OAuth extension point in gerrit (blame /me for that) the user 101 | would only see "Unauthorized" message. 102 | 103 | By default the Google OAuth provider will not set a username (used for ssh) and 104 | the user can choose one from the web ui (needed before using ssh). It is possible 105 | to automatically use the user part from the google apps email. This is deactivated 106 | by default. To activate it, add: 107 | 108 | ``` 109 | plugin.gerrit-oauth-provider-google-oauth.use-email-as-username = true 110 | ``` 111 | 112 | Note: the usernames are unique in gerrit. If a username already exists this will 113 | be ignored and the user will have to choose a different one from the web ui. 114 | 115 | ### CAS OAuth 116 | 117 | For CAS OAuth setting 118 | 119 | ``` 120 | plugin.gerrit-oauth-provider-cas-oauth.root-url = "https://example.com/cas" 121 | ``` 122 | 123 | is required, since CAS is a self-hosted application. 124 | 125 | Note that the CAS OAuth plugin only supports CAS V5 and higher. 126 | 127 | The plugin expects CAS to make several attributes available to it: 128 | 129 | | Name | Description | Required | 130 | |---|---|---| 131 | | id | External ID | yes | 132 | | login | Login name | no | 133 | | email | Email address | no | 134 | | name | Display name | no | 135 | 136 | ### CoreOS Dex OAuth 137 | 138 | For Dex OAuth setting 139 | 140 | ``` 141 | plugin.gerrit-oauth-provider-dex-oauth.root-url = "https://example.com" 142 | ``` 143 | 144 | is required, since Dex is a self-hosted application. 145 | 146 | ## Obtaining provider authorizations 147 | 148 | ### Google 149 | 150 | To obtain client-id and client-secret for Google OAuth, go to 151 | [Google Developers Console](https://console.developers.google.com): 152 | 153 | - Create a project 154 | 155 | ![Create a porject](images/google-1.png) 156 | 157 | - Go inside the created project 158 | 159 | - In "APIs & auth"/"Credentials" select "Create new Client ID" and 160 | create Client ID for a Web application 161 | 162 | ![Create Client ID for a Web application](images/google-2.png) 163 | 164 | - Enter additional information about the project, which will be 165 | presented to user during the authentication process 166 | 167 | ![Enter additional information](images/google-3.png) 168 | 169 | - Specify authorized redirect URL: `/oauth` 170 | 171 | ![Specify authorized redirect URI](images/google-4.png) 172 | 173 | After the final step, the page will show generated client id and 174 | secret. 175 | 176 | ![Generated id and secret](images/google-5.png) 177 | 178 | ### GitHub 179 | 180 | To obtain client-id and client-secret for GitHub OAuth, go to 181 | [Applications settings in your GitHub account](https://github.com/settings/applications): 182 | 183 | - Select "Register new application" and enter information about the 184 | application. 185 | 186 | Note that it is important that authorization callback URL points to 187 | `/oauth`. 188 | 189 | ![Register new application on GitHub](images/github-1.png) 190 | 191 | 192 | After application is registered, the page will show generated client id and 193 | secret. 194 | 195 | ![Generated client id and secret](images/github-2.png) 196 | 197 | ### CAS 198 | 199 | The client-id and client-secret for CAS OAuth are part of the CAS 200 | service definition and need to be set manually. 201 | 202 | See 203 | [the CAS documentation](https://apereo.github.io/cas/4.2.x/installation/OAuth-OpenId-Authentication.html#add-oauth-clients) 204 | for an example. 205 | 206 | ### GitLab 207 | 208 | To obtain client-id and client-secret for GitLab OAuth, go to 209 | Applications settings in your GitLab profile: 210 | 211 | - Select "Save application" and enter information about the 212 | application. 213 | 214 | Note that it is important that Redirect URI points to 215 | `/oauth`. 216 | 217 | ![Save new application on GitLab](images/gitlab-1.png) 218 | 219 | 220 | After application is saved, the page will show generated client id and 221 | secret. 222 | 223 | ![Generated client id and secret](images/gitlab-2.png) 224 | 225 | ### CoreOS Dex 226 | 227 | The client-id and client-secret for Dex OAuth are part of the Dex 228 | setup and need to be set manually. 229 | 230 | See 231 | [Using Dex](https://github.com/coreos/dex/blob/master/Documentation/using-dex.md) 232 | for an example. 233 | 234 | ### AirVantage 235 | 236 | The client-id and client-secret for AirVantage OAuth can be obtained by registering 237 | a Client application. 238 | See [Getting Started](https://source.sierrawireless.com/airvantage/av/howto/cloud/gettingstarted_api). 239 | 240 | ### Phabricator 241 | 242 | The client-id and client-secret for Phabricator can be obtained by registering a 243 | Client application. 244 | See [Using the Phabricator OAuth Server](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/). 245 | 246 | ### Azure (previously named Office365) 247 | Were previously named Office365 but both `plugin.gerrit-oauth-provider-azure-oauth` and 248 | `plugin.gerrit-oauth-provider-office365-oauth` is supported by the Azure OAuth. 249 | When running *java gerrit.war init* it will check the existing config to see if it finds the old 250 | naming and use that during the init run, if it does not find the `office365-oauth` it will 251 | use the new `azure-oauth` naming. 252 | 253 | The client-id and client-secret for Azure can be obtained by registering a new application, 254 | see [OAuth 2.0 and OpenID Connect protocols on Microsoft identity platform](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols). 255 | 256 | ####Username 257 | By default, Azure OAuth will not set a username (used for ssh) and the user can choose one from the web ui 258 | can be used. 259 | ``` 260 | plugin.gerrit-oauth-provider-azure-oauth.use-email-as-username = true 261 | ``` 262 | 263 | ####Tenant 264 | The Azure OAuth is default set to use the tenant `organizations` but a specific tenant can be used by 265 | the option `tenant`. If a tenant other than `common`, `organizations` or `consumers` is used then the tokens will be 266 | validated that they are originating from the same tenant that is configured in the Gerrit OAuth plugin. 267 | See [Microsoft identity platform and OpenID Connect protocol](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#fetch-the-openid-connect-metadata-document) 268 | ``` 269 | plugin.gerrit-oauth-provider-azure-oauth.tenant = 270 | ``` 271 | 272 | Regardless of tenant all tokens will be checked that they contain the client_id set 273 | in the Azure OAuth. 274 | 275 | ####Migrating from Office365 naming 276 | If this where previously installed with the `office365-oauth` you can migrate to `azure-oauth` by setting the 277 | flag. 278 | ``` 279 | plugin.gerrit-oauth-provider-azure-oauth.link-to-existing-office365-accounts = true 280 | ``` 281 | This will try to link the old `office365-oauth` external id to the new `azure-oauth` external id automatically. 282 | Another option is to migrate these manually offline, see [External IDs](https://gerrit-review.googlesource.com/Documentation/config-accounts.html#external-ids) 283 | for more information. 284 | 285 | ### Keycloak 286 | 287 | When setting up a client in Keycloak for Gerrit, enter a value for the *Client ID* and ensure you choose the `openid-connect` 288 | protocol and select the `confidential` access type. Once you click save, a *Credentials* tab will appear where you will find 289 | the Secret. 290 | 291 | The root URL will the protocol and hostname of your Keycloak instance (for example, https://signon.example.com). 292 | 293 | You can optionally set `use-preferred-username = false` if you would prefer to not have the `preferred_username` 294 | token be automatically set as the users username, and instead let users choose their own usernames. 295 | -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/github-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/github-1.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/github-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/github-2.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/gitlab-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/gitlab-1.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/gitlab-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/gitlab-2.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/google-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/google-1.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/google-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/google-2.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/google-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/google-3.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/google-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/google-4.png -------------------------------------------------------------------------------- /src/main/resources/Documentation/images/google-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davido/gerrit-oauth-provider/d21d1721f15e6dc0db4145724e608d5c416e692d/src/main/resources/Documentation/images/google-5.png -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/AirVantageApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class AirVantageApiTest { 24 | private AirVantageApi api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new AirVantageApi(); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/BitbucketApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class BitbucketApiTest { 24 | private BitbucketApi api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new BitbucketApi(); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/CasApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class CasApiTest { 24 | private CasApi api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new CasApi(""); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/DexApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class DexApiTest { 24 | private DexApi api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new DexApi(""); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/Facebook2ApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class Facebook2ApiTest { 24 | private Facebook2Api api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new Facebook2Api(); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/GitHub2ApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class GitHub2ApiTest { 24 | private GitHub2Api api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new GitHub2Api(GitHubOAuthService.GITHUB_ROOT_URL); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/GitLabApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class GitLabApiTest { 24 | private GitLabApi api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new GitLabApi(""); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/GithubApiUrlTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | import static org.mockito.Mockito.when; 19 | 20 | import com.google.common.base.CharMatcher; 21 | import com.google.common.base.Strings; 22 | import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 23 | import com.google.gerrit.server.config.PluginConfig; 24 | import com.google.gerrit.server.config.PluginConfigFactory; 25 | import com.google.inject.Provider; 26 | import java.net.URLEncoder; 27 | import java.nio.charset.StandardCharsets; 28 | import org.eclipse.jgit.lib.Config; 29 | import org.junit.Test; 30 | import org.junit.runner.RunWith; 31 | import org.mockito.Mock; 32 | import org.mockito.junit.MockitoJUnitRunner; 33 | 34 | @RunWith(MockitoJUnitRunner.class) 35 | public class GithubApiUrlTest { 36 | private static final String PLUGIN_NAME = "gerrit-oauth-provider"; 37 | private static final String CANONICAL_URL = "https://localhost"; 38 | private static final String TEST_CLIENT_ID = "test_client_id"; 39 | 40 | @Mock private PluginConfigFactory pluginConfigFactoryMock; 41 | @Mock private Provider urlProviderMock; 42 | 43 | private OAuthServiceProvider getGithubOAuthProvider(String rootUrl) { 44 | PluginConfig.Update pluginConfig = 45 | PluginConfig.Update.forTest(PLUGIN_NAME + GitHubOAuthService.CONFIG_SUFFIX, new Config()); 46 | if (!Strings.isNullOrEmpty(rootUrl)) { 47 | pluginConfig.setString(InitOAuth.ROOT_URL, rootUrl); 48 | } 49 | pluginConfig.setString(InitOAuth.CLIENT_ID, TEST_CLIENT_ID); 50 | pluginConfig.setString(InitOAuth.CLIENT_SECRET, "secret"); 51 | when(pluginConfigFactoryMock.getFromGerritConfig( 52 | PLUGIN_NAME + GitHubOAuthService.CONFIG_SUFFIX)) 53 | .thenReturn(pluginConfig.asPluginConfig()); 54 | when(urlProviderMock.get()).thenReturn(CANONICAL_URL); 55 | 56 | return new GitHubOAuthService(pluginConfigFactoryMock, PLUGIN_NAME, urlProviderMock); 57 | } 58 | 59 | private String getExpectedUrl(String rootUrl) throws Exception { 60 | if (rootUrl == null) { 61 | rootUrl = GitHubOAuthService.GITHUB_ROOT_URL; 62 | } 63 | rootUrl = CharMatcher.is('/').trimTrailingFrom(rootUrl) + "/"; 64 | return String.format( 65 | "%slogin/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s%s&scope=%s", 66 | rootUrl, 67 | TEST_CLIENT_ID, 68 | URLEncoder.encode(CANONICAL_URL, StandardCharsets.UTF_8.name()), 69 | URLEncoder.encode("/oauth", StandardCharsets.UTF_8.name()), 70 | URLEncoder.encode(GitHubOAuthService.SCOPE, StandardCharsets.UTF_8.name())); 71 | } 72 | 73 | @Test 74 | public void nullUrlIsLoaded() throws Exception { 75 | String rootUrl = null; 76 | OAuthServiceProvider provider = getGithubOAuthProvider(rootUrl); 77 | assertThat(provider.getAuthorizationUrl()).isEqualTo(getExpectedUrl(rootUrl)); 78 | } 79 | 80 | @Test 81 | public void githubUrlIsLoaded() throws Exception { 82 | String rootUrl = "https://github.com"; 83 | OAuthServiceProvider provider = getGithubOAuthProvider(rootUrl); 84 | assertThat(provider.getAuthorizationUrl()).isEqualTo(getExpectedUrl(rootUrl)); 85 | } 86 | 87 | @Test 88 | public void githubUrlWithTrailingSlashIsLoaded() throws Exception { 89 | String rootUrl = "https://github.com/"; 90 | OAuthServiceProvider provider = getGithubOAuthProvider(rootUrl); 91 | assertThat(provider.getAuthorizationUrl()).isEqualTo(getExpectedUrl(rootUrl)); 92 | } 93 | 94 | @Test 95 | public void gheUrlIsLoaded() throws Exception { 96 | String rootUrl = "https://git.yourcompany.com"; 97 | OAuthServiceProvider provider = getGithubOAuthProvider(rootUrl); 98 | assertThat(provider.getAuthorizationUrl()).isEqualTo(getExpectedUrl(rootUrl)); 99 | } 100 | 101 | @Test 102 | public void gheUrlWithTrailingSlashIsLoaded() throws Exception { 103 | String rootUrl = "https://git.yourcompany.com/"; 104 | OAuthServiceProvider provider = getGithubOAuthProvider(rootUrl); 105 | assertThat(provider.getAuthorizationUrl()).isEqualTo(getExpectedUrl(rootUrl)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/Google2ApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class Google2ApiTest { 24 | private Google2Api api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new Google2Api(); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/KeycloakApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class KeycloakApiTest { 24 | private KeycloakApi api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new KeycloakApi("", ""); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/MicrosoftAzureActiveDirectory20ApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.apis.MicrosoftAzureActiveDirectory20Api; 20 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | 24 | public class MicrosoftAzureActiveDirectory20ApiTest { 25 | private MicrosoftAzureActiveDirectory20Api api; 26 | 27 | @Before 28 | public void setUp() { 29 | api = MicrosoftAzureActiveDirectory20Api.instance(); 30 | } 31 | 32 | @Test 33 | public void testAccessTokenExtractor() { 34 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApiTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 The Android Open Source Project 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.googlesource.gerrit.plugins.oauth; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class PhabricatorApiTest { 24 | private PhabricatorApi api; 25 | 26 | @Before 27 | public void setUp() { 28 | api = new PhabricatorApi(""); 29 | } 30 | 31 | @Test 32 | public void testAccessTokenExtractor() { 33 | assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tools/BUILD: -------------------------------------------------------------------------------- 1 | # Empty file - bazel treat directories with BUILD file as a package -------------------------------------------------------------------------------- /tools/bzl/BUILD: -------------------------------------------------------------------------------- 1 | # Empty file required by Bazel 2 | -------------------------------------------------------------------------------- /tools/bzl/classpath.bzl: -------------------------------------------------------------------------------- 1 | load( 2 | "@com_googlesource_gerrit_bazlets//tools:classpath.bzl", 3 | _classpath_collector = "classpath_collector", 4 | ) 5 | 6 | classpath_collector = _classpath_collector 7 | -------------------------------------------------------------------------------- /tools/bzl/junit.bzl: -------------------------------------------------------------------------------- 1 | load( 2 | "@com_googlesource_gerrit_bazlets//tools:junit.bzl", 3 | _junit_tests = "junit_tests", 4 | ) 5 | 6 | junit_tests = _junit_tests 7 | -------------------------------------------------------------------------------- /tools/bzl/maven_jar.bzl: -------------------------------------------------------------------------------- 1 | load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", _maven_jar = "maven_jar") 2 | 3 | maven_jar = _maven_jar 4 | -------------------------------------------------------------------------------- /tools/bzl/plugin.bzl: -------------------------------------------------------------------------------- 1 | load( 2 | "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl", 3 | _gerrit_plugin = "gerrit_plugin", 4 | _plugin_deps = "PLUGIN_DEPS", 5 | _plugin_test_deps = "PLUGIN_TEST_DEPS", 6 | ) 7 | 8 | gerrit_plugin = _gerrit_plugin 9 | PLUGIN_DEPS = _plugin_deps 10 | PLUGIN_TEST_DEPS = _plugin_test_deps 11 | -------------------------------------------------------------------------------- /tools/eclipse/BUILD: -------------------------------------------------------------------------------- 1 | load("//tools/bzl:classpath.bzl", "classpath_collector") 2 | 3 | classpath_collector( 4 | name = "main_classpath_collect", 5 | testonly = 1, 6 | deps = [ 7 | "//:gerrit-oauth-provider__plugin_test_deps", 8 | ], 9 | ) 10 | -------------------------------------------------------------------------------- /tools/eclipse/project.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (C) 2017 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | `bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n oauth -r . 16 | -------------------------------------------------------------------------------- /tools/workspace_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This script will be run by bazel when the build process starts to 4 | # generate key-value information that represents the status of the 5 | # workspace. The output should be like 6 | # 7 | # KEY1 VALUE1 8 | # KEY2 VALUE2 9 | # 10 | # If the script exits with non-zero code, it's considered as a failure 11 | # and the output will be discarded. 12 | 13 | from __future__ import print_function 14 | import subprocess 15 | import sys 16 | 17 | CMD = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty'] 18 | 19 | 20 | def revision(): 21 | try: 22 | return subprocess.check_output(CMD).strip().decode("utf-8") 23 | except OSError as err: 24 | print('could not invoke git: %s' % err, file=sys.stderr) 25 | sys.exit(1) 26 | except subprocess.CalledProcessError as err: 27 | print('error using git: %s' % err, file=sys.stderr) 28 | sys.exit(1) 29 | 30 | 31 | print("STABLE_BUILD_OAUTH_LABEL %s" % revision()) 32 | --------------------------------------------------------------------------------