[GEOS-11272] spring-security-oauth replacement, with spring-security 5.8 (#8569)

* [GS-11272] Created new oidc module as copy of unchanged GS 2.26 code

* [GS-11272] New implementation of gs-sec-oidc based on spring-security-oauth2

* [GEOS-11272] Bump to 2.28-SNAPSHOT

* [GEOS-11272] Add unit test

* [GEOS-11272] Fix typos

* [GEOS-11272] Export drawio diagrams

---------

Co-authored-by: Andreas Watermeyer <Andreas.Watermeyer@its-digital.de>
This commit is contained in:
Cécile Vuilleumier 2025-07-03 15:27:17 +02:00 committed by GitHub
parent b99273fcda
commit e1f71a6d33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
91 changed files with 10002 additions and 101 deletions

5
.gitignore vendored
View File

@ -9,6 +9,8 @@ target
*.class
*.pyc
.pmd
# draw.io backup
*.bkp
/.metadata/
@ -46,3 +48,6 @@ src/release/installer/win/target/
# Eclipse stuff
src/web/app/.temp-Start*
# VS Code
.vscode/

View File

@ -375,6 +375,12 @@
<module>security</module>
</modules>
</profile>
<profile>
<id>oidc</id>
<modules>
<module>security</module>
</modules>
</profile>
<profile>
<id>oseo</id>
<modules>

View File

@ -0,0 +1,304 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/26.0.4 Chrome/128.0.6613.186 Electron/32.2.5 Safari/537.36" version="26.0.4">
<diagram name="Page-1" id="2YBvvXClWsGukQMizWep">
<mxGraphModel dx="1434" dy="838" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="HcqALuEwrhDAun1lp61i-4" value="User" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;spacingTop=-3;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="90" y="60" width="300" height="70" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-3" value="Login-Provider" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;spacingTop=-3;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="950" y="60" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-2" value="GeoServer" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;spacingTop=-3;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="420" y="60" width="500" height="70" as="geometry" />
</mxCell>
<mxCell id="aM9ryv3xv72pqoxQDRHE-1" value="OAuth2Authorization-RequestRedirectFilter" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="435" y="80" width="130" height="720" as="geometry" />
</mxCell>
<mxCell id="aM9ryv3xv72pqoxQDRHE-2" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="aM9ryv3xv72pqoxQDRHE-1" vertex="1">
<mxGeometry x="60" y="160" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="aM9ryv3xv72pqoxQDRHE-5" value="Authorization-&lt;br&gt;Server" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};fillColor=#bac8d3;strokeColor=#23445d;" parent="1" vertex="1">
<mxGeometry x="960" y="80" width="100" height="720" as="geometry" />
</mxCell>
<mxCell id="aM9ryv3xv72pqoxQDRHE-6" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="aM9ryv3xv72pqoxQDRHE-5" vertex="1">
<mxGeometry x="45" y="239" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-26" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="aM9ryv3xv72pqoxQDRHE-5" vertex="1">
<mxGeometry x="45" y="319" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-30" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="aM9ryv3xv72pqoxQDRHE-5" vertex="1">
<mxGeometry x="45" y="399" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-37" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="aM9ryv3xv72pqoxQDRHE-5" vertex="1">
<mxGeometry x="45" y="479" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="Br-kYopsOSGonpuqLUsM-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;dashed=1;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="140" y="800" as="targetPoint" />
<mxPoint x="140" y="130" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="Br-kYopsOSGonpuqLUsM-4" value="Resource Owner" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
<mxGeometry x="133" y="76" width="15" height="30" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-1" value="Browser" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="300" y="80" width="80" height="720" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-2" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="R4H-Cz9XJfiXXCovYyHO-1" vertex="1">
<mxGeometry x="34" y="80" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-21" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="R4H-Cz9XJfiXXCovYyHO-1" vertex="1">
<mxGeometry x="34" y="280" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-27" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="R4H-Cz9XJfiXXCovYyHO-1" vertex="1">
<mxGeometry x="35" y="360" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-9" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="R4H-Cz9XJfiXXCovYyHO-1" vertex="1">
<mxGeometry x="35" y="200" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-5" value="/" style="html=1;verticalAlign=bottom;startArrow=oval;startFill=1;endArrow=block;startSize=8;curved=0;rounded=0;" parent="1" target="R4H-Cz9XJfiXXCovYyHO-2" edge="1">
<mxGeometry width="60" relative="1" as="geometry">
<mxPoint x="140" y="160" as="sourcePoint" />
<mxPoint x="200" y="160" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-6" value="open Geoserver Admin UI" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="R4H-Cz9XJfiXXCovYyHO-5" vertex="1" connectable="0">
<mxGeometry x="-0.0444" y="8" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-7" value="Dispatcher&lt;br&gt;-Servlet" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="780" y="80" width="130" height="720" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-8" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="R4H-Cz9XJfiXXCovYyHO-7" vertex="1">
<mxGeometry x="60" y="80" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-41" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="R4H-Cz9XJfiXXCovYyHO-7" vertex="1">
<mxGeometry x="60" y="640" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-11" value="/geoserver/web/" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;" parent="1" target="R4H-Cz9XJfiXXCovYyHO-7" edge="1">
<mxGeometry x="-0.8056" width="80" relative="1" as="geometry">
<mxPoint x="330" y="160" as="sourcePoint" />
<mxPoint x="410" y="160" as="targetPoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-14" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="1" vertex="1">
<mxGeometry x="334" y="200" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-10" value="LoginPage" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" parent="1" target="R4H-Cz9XJfiXXCovYyHO-1" edge="1">
<mxGeometry x="0.825" relative="1" as="geometry">
<mxPoint x="844.5" y="200" as="sourcePoint" />
<mxPoint x="370" y="200" as="targetPoint" />
<Array as="points">
<mxPoint x="460" y="200" />
</Array>
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-12" value="click Login button for &amp;lt;clientRegId&amp;gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;" parent="1" target="R4H-Cz9XJfiXXCovYyHO-14" edge="1">
<mxGeometry width="80" relative="1" as="geometry">
<mxPoint x="140" y="240" as="sourcePoint" />
<mxPoint x="325" y="210" as="targetPoint" />
<Array as="points">
<mxPoint x="260" y="240" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="aM9ryv3xv72pqoxQDRHE-3" value="&lt;font style=&quot;color: rgb(255, 0, 0);&quot;&gt;/geoserver/oauth2/authorization/&amp;lt;clientRegId&amp;gt;&lt;/font&gt;" style="html=1;verticalAlign=bottom;startArrow=none;endArrow=block;startSize=8;edgeStyle=elbowEdgeStyle;elbow=vertical;curved=0;rounded=0;startFill=0;labelBackgroundColor=none;align=left;" parent="1" edge="1">
<mxGeometry x="-0.9205" relative="1" as="geometry">
<mxPoint x="344" y="240" as="sourcePoint" />
<mxPoint as="offset" />
<mxPoint x="495" y="240" as="targetPoint" />
<Array as="points">
<mxPoint x="360" y="240" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-15" value="OAuth2Login-&lt;br&gt;AuthenticationFilter" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="595" y="81" width="130" height="719" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-38" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="R4H-Cz9XJfiXXCovYyHO-15" vertex="1">
<mxGeometry x="60" y="398" width="10" height="281" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-17" value="redirect" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="499.5" y="280" as="sourcePoint" />
<mxPoint x="340.0999999999999" y="280" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-18" value="/&amp;lt;authorizationEndpoint&amp;gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;align=left;" parent="1" edge="1">
<mxGeometry x="-0.9696" y="-1" width="80" relative="1" as="geometry">
<mxPoint x="339.81034482758605" y="319" as="sourcePoint" />
<mxPoint x="1009.5" y="319" as="targetPoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-19" value="Authorization Server Login Page" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;align=left;" parent="1" edge="1">
<mxGeometry x="0.9397" relative="1" as="geometry">
<mxPoint x="1009.5" y="359" as="sourcePoint" />
<mxPoint x="339.81034482758605" y="359" as="targetPoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-20" value="enter credentials" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;" parent="1" target="R4H-Cz9XJfiXXCovYyHO-1" edge="1">
<mxGeometry width="80" relative="1" as="geometry">
<mxPoint x="140" y="400" as="sourcePoint" />
<mxPoint x="290" y="400" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-22" value="/&amp;lt;login&amp;gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;align=left;" parent="1" source="R4H-Cz9XJfiXXCovYyHO-1" edge="1">
<mxGeometry x="-0.9696" y="-1" width="80" relative="1" as="geometry">
<mxPoint x="360.00034482758605" y="401" as="sourcePoint" />
<mxPoint x="1010" y="400" as="targetPoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-25" value="redirect" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1009.5" y="440" as="sourcePoint" />
<mxPoint x="339.81034482758605" y="440" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-28" value="&lt;font style=&quot;color: rgb(255, 0, 0);&quot;&gt;/geoserver/login/oauth2/code/&amp;lt;clientRegId&amp;gt;&lt;/font&gt;" style="html=1;verticalAlign=bottom;startArrow=none;endArrow=block;startSize=8;edgeStyle=elbowEdgeStyle;elbow=vertical;curved=0;rounded=0;startFill=0;labelBackgroundColor=none;align=left;" parent="1" source="R4H-Cz9XJfiXXCovYyHO-1" target="R4H-Cz9XJfiXXCovYyHO-15" edge="1">
<mxGeometry x="-0.9205" relative="1" as="geometry">
<mxPoint x="390" y="480" as="sourcePoint" />
<mxPoint as="offset" />
<mxPoint x="541" y="480" as="targetPoint" />
<Array as="points">
<mxPoint x="406" y="480" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-29" value="/&amp;lt;tokenEndpoint&amp;gt;" style="html=1;verticalAlign=bottom;startArrow=none;endArrow=block;startSize=8;edgeStyle=elbowEdgeStyle;elbow=vertical;curved=0;rounded=0;startFill=0;labelBackgroundColor=none;align=left;" parent="1" target="aM9ryv3xv72pqoxQDRHE-5" edge="1">
<mxGeometry x="-0.9428" relative="1" as="geometry">
<mxPoint x="660" y="480" as="sourcePoint" />
<mxPoint as="offset" />
<mxPoint x="990" y="480" as="targetPoint" />
<Array as="points">
<mxPoint x="736" y="480" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-31" value="" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1009.5" y="520" as="sourcePoint" />
<mxPoint x="659.810344827586" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-33" value="userInfo" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1009.5" y="677" as="sourcePoint" />
<mxPoint x="659.810344827586" y="677" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-34" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;edgeStyle&quot;:&quot;elbowEdgeStyle&quot;,&quot;elbow&quot;:&quot;vertical&quot;,&quot;curved&quot;:0,&quot;rounded&quot;:0};" parent="1" vertex="1">
<mxGeometry x="1005" y="638" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-35" value="/&amp;lt;jwkSetEndpoint&amp;gt;" style="html=1;verticalAlign=bottom;startArrow=none;endArrow=block;startSize=8;edgeStyle=elbowEdgeStyle;elbow=vertical;curved=0;rounded=0;startFill=0;labelBackgroundColor=none;align=left;" parent="1" source="R4H-Cz9XJfiXXCovYyHO-15" target="aM9ryv3xv72pqoxQDRHE-5" edge="1">
<mxGeometry x="-0.9205" relative="1" as="geometry">
<mxPoint x="670" y="560" as="sourcePoint" />
<mxPoint as="offset" />
<mxPoint x="1020" y="560" as="targetPoint" />
<Array as="points">
<mxPoint x="746" y="560" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-36" value="jwkSet" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" parent="1" target="R4H-Cz9XJfiXXCovYyHO-15" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1010" y="600" as="sourcePoint" />
<mxPoint x="700.0003448275861" y="600" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-39" value="GeoServer Admin UI Page" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" parent="1" source="R4H-Cz9XJfiXXCovYyHO-7" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="659.75" y="760" as="sourcePoint" />
<mxPoint x="340.25" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-40" value="invokeChain" style="html=1;verticalAlign=bottom;startArrow=none;endArrow=block;startSize=8;edgeStyle=elbowEdgeStyle;elbow=vertical;curved=0;rounded=0;startFill=0;labelBackgroundColor=none;align=left;" parent="1" source="R4H-Cz9XJfiXXCovYyHO-15" target="R4H-Cz9XJfiXXCovYyHO-7" edge="1">
<mxGeometry x="-0.9188" relative="1" as="geometry">
<mxPoint x="680" y="720" as="sourcePoint" />
<mxPoint as="offset" />
<mxPoint x="1030" y="720" as="targetPoint" />
<Array as="points">
<mxPoint x="756" y="720" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="R4H-Cz9XJfiXXCovYyHO-32" value="/&amp;lt;userInfoEndpoint&amp;gt;" style="html=1;verticalAlign=bottom;startArrow=none;endArrow=block;startSize=8;edgeStyle=elbowEdgeStyle;elbow=vertical;curved=0;rounded=0;startFill=0;labelBackgroundColor=none;align=left;exitX=0.5;exitY=0.703;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" edge="1">
<mxGeometry x="-0.9205" relative="1" as="geometry">
<mxPoint x="660" y="638" as="sourcePoint" />
<mxPoint as="offset" />
<mxPoint x="1010" y="638" as="targetPoint" />
<Array as="points">
<mxPoint x="736" y="638" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-5" value="&lt;u&gt;&lt;font style=&quot;font-size: 20px;&quot;&gt;GeoServer Spring OAuth / OpenID Login Process&lt;/font&gt;&lt;/u&gt;&lt;div&gt;&lt;span style=&quot;font-weight: normal;&quot;&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;Status 01/2025&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fontStyle=1;align=right;strokeColor=none;fillColor=none;" parent="1" vertex="1">
<mxGeometry x="210" width="540" height="60" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-6" value="OAuth2 Authorization Code Flow" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontStyle=1;fontSize=18;" parent="1" vertex="1">
<mxGeometry x="765" y="7" width="285" height="30" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-7" value="Obtain&lt;font style=&quot;color: rgb(0, 0, 255);&quot;&gt; &lt;b&gt;Authorization Code&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;horizontal=0;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="29" y="140" width="50" height="320" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-8" value="Obtain&lt;font style=&quot;color: rgb(0, 102, 0);&quot;&gt;&lt;b&gt; Access Token / ID Token&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;horizontal=0;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="29" y="460" width="50" height="320" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-11" value="Authorization Code" style="image;sketch=0;aspect=fixed;html=1;points=[];align=left;fontSize=12;image=img/lib/mscae/Certificate.svg;spacingTop=-20;spacingLeft=20;imageBackground=none;fontColor=#0000FF;" parent="1" vertex="1">
<mxGeometry x="709" y="420" width="16" height="13.44" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-13" value="&lt;font style=&quot;color: rgb(0, 102, 0);&quot;&gt;Access Token [ + ID Token ]&lt;/font&gt;" style="image;sketch=0;aspect=fixed;html=1;points=[];align=left;fontSize=12;image=img/lib/mscae/Certificate.svg;spacingTop=-20;spacingLeft=20;imageBackground=none;fontColor=#0000FF;" parent="1" vertex="1">
<mxGeometry x="800" y="504" width="16" height="13.44" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-14" value="Example:&lt;br&gt;http://localhost:9000" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=12;align=left;fillColor=#fff2cc;strokeColor=#D6B656;spacingLeft=2;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1120" y="160" width="320" height="40" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-15" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;strokeColor=#6D5100;" parent="1" source="HcqALuEwrhDAun1lp61i-14" target="HcqALuEwrhDAun1lp61i-3" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-16" value="Example:&lt;br&gt;http://localhost:9000/oauth2/authorize?&lt;br&gt;&amp;nbsp;response_type=code&amp;amp;&lt;br&gt;&amp;nbsp;client_id=geoserver&amp;amp;&lt;br&gt;&amp;nbsp;scope=openid&amp;amp;&lt;div&gt;&amp;nbsp;state=WqjM[...]%3D&amp;amp;&lt;br&gt;&amp;nbsp;redirect_uri=http://localhost:8080/geoserver/login/oauth2/code/oidc&amp;amp;&lt;br&gt;&amp;nbsp;nonce=HaS0is[...]&lt;/div&gt;" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=12;align=left;fillColor=#fff2cc;strokeColor=#d6b656;spacingLeft=2;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1120" y="220" width="320" height="140" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-17" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;dashed=1;strokeColor=#6D5100;startArrow=none;startFill=0;" parent="1" source="HcqALuEwrhDAun1lp61i-16" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="450" y="280" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-18" value="Example:&lt;br&gt;http://localhost:8080/geoserver/login/oauth2/code/oidc? &lt;br&gt;&amp;nbsp;code=gNR1OMrUaQtBSMozB7cSHWTAAUscBS[...]j&amp;amp;&lt;br&gt;&amp;nbsp;state=WqjM[...]%3D" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=12;align=left;fillColor=#fff2cc;strokeColor=#d6b656;spacingLeft=2;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1120" y="381.72" width="320" height="90" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-19" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;dashed=1;strokeColor=#6D5100;" parent="1" source="HcqALuEwrhDAun1lp61i-18" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="610" y="480" as="targetPoint" />
<mxPoint x="1130" y="298" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-20" value="Called only if token uses a signatures algorithm (rather than MAC algorithm) the public keys for signature validation are obtained from jwksetEndpoint" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=12;align=left;fillColor=#fff2cc;strokeColor=#d6b656;spacingLeft=2;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1120" y="490" width="320" height="60" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-21" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;dashed=1;strokeColor=#6D5100;" parent="1" source="HcqALuEwrhDAun1lp61i-20" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="920" y="560" as="targetPoint" />
<mxPoint x="1130" y="449" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-22" value="Called only if &lt;br&gt;* no ID Token was obtained (provider does not support OpenID Connect, currently GitHub) or &lt;br&gt;* call is required due to involved scopes (see OidcUserService)&lt;br&gt;* using role source userInfo (currently separate call)" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=12;align=left;fillColor=#fff2cc;strokeColor=#d6b656;spacingLeft=2;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1120" y="560" width="320" height="100" as="geometry" />
</mxCell>
<mxCell id="HcqALuEwrhDAun1lp61i-23" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;dashed=1;strokeColor=#6D5100;exitX=0.006;exitY=0.333;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="HcqALuEwrhDAun1lp61i-22" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="910" y="640" as="targetPoint" />
<mxPoint x="1130" y="548" as="sourcePoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 893 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@ -0,0 +1,29 @@
# GeoServer OAuth2 / OpenID Connect diagrams
Diagram(s) here have been created using draw.io.
Options for viewing and editing:
## View directly
View diagrams directly (maybe adjust URL, to use proper repository and branch).
Class diagram:
https://www.draw.io/?lightbox=1&edit=_blank#Uhttps://raw.githubusercontent.com/geoserver/geoserver/refs/heads/main/src/community/security/oidc/doc/diagrams/GeoServer-Spring-OAuth.drawio
Sequence diagram:
https://www.draw.io/?lightbox=1&edit=_blank#Uhttps://raw.githubusercontent.com/geoserver/geoserver/refs/heads/main/src/community/security/oidc/doc/diagrams/GeoServer-Spring-OAuth-Seq.drawio
## Edit and view on draw.io website
Alternatively open the diagram in the draw.io website, either from local file or open with github integration:
https://app.diagrams.net
## Edit and view using the locally installed application
Download free editor and viewer app, for one of the typical platforms, currently:
https://github.com/jgraph/drawio-desktop/releases/tag/v25.0.2

View File

@ -0,0 +1,114 @@
# GeoServer OpenID Connect Rewrite
**Status:** January 2025
## Current Status
- The implementation is ready for review and public testing.
- Existing unit tests have been ported and supplemented, except for functionality that has been dropped (see details below). Test coverage (excluding "Resource Server" UCS, see below)
- 82% of instruction for gs-sec-oidc-core
- 93% of instruction for gs-sec-oidc-web
## Features
- Working with Google, GitHub, Microsoft Azure, and one custom OIDC provider.
- The "Resource Server" functionality (i.e., "OpenID Connect With Attached Access Bearer Tokens") was originally available. However, it was decided not to support this feature for the time being, as a separate extension (~ `gs-sec-jwt`) already provides similar functionality.
- The current extension still contains code for the "Resource Server" use case, but it is in an initial status.
- A first test of the feature was successful, but the functionality is currently disabled (commented out in `application.xml`).
- Consider removing this code if it remains unused (though this might be unfortunate).
## Design & Goals
- Leverage Springs public API to configure Spring filters and associated classes for a future-proof solution.
- Filters are created by Spring and integrated into GeoServer, minimizing custom setup code.
- While Spring is not inherently designed for the dynamic behavior required by GeoServer, the approach still appears reasonable.
- Avoid "fishing" for request parameters and headers.
### References
- For further information, refer to JavaDocs:
- `org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationProvider`
- `org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter`
- See the class diagram in `gs-sec-oidc/doc/diagrams` for orientation.
## Installation
- The project includes an assembly module. Place the JAR files in `WEB-INF/lib`.
## Configuration
1. Create a filter named **"openid-connect"** using the Admin Web UI.
2. Add the **"openid-connect"** filter to the `web` filter chain.
3. Create a new filter chain named **"oauth-callback"** with the following configuration:
- **Ant Patterns:** `/oauth2/authorization/**,/login/oauth2/code/**` (Attention: no space between)
- Assign the **"openid-connect"** filter to this chain.
- Use default settings for everything else.
- Ensure the chain is positioned before `webLogin`.
4. If using a user group service as the role source, create users with roles that correspond to the identity provider's users.
5. Set up the identity provider (Google, GitHub, Microsoft, or custom OIDC provider) as usual.
## Open Tasks
### High Relevance
- Update the user guide.
- generally
- specifics:
- special cases for `tokenRolesClaim` (`scope` for source `access token` and `authorities` for source `userInfo`, see `GeoServerOAuth2RoleResolver`)
- Validation: Previously `checkTokenEndpointUrl` or `jwkURI` was required, see prior OpenIdConnectFilterConfigValidator. Situation is a little confusing.
- What I understand:
- `checkTokenEndpointUrl` refers to `userInfoUri` (according to Spring naming). API purpose: Exchange an access token into userInfo data.
- `jwkURI` refers to `jwkSetUri` (according to Spring naming). API purpose: Load IDP public keys for signature validation.
- I suppose the prior code is using one of those methods to validate the access token.
- The current Spring approach is different:
- Spring uses the `jwkSetUri` for validation by default, which is determined by the selected JwsAlgorithm, which defaults to RS256. This algorithm belongs to the family of signature algorithms using asymetric encryption and public/private keys. Therefor the public keys are loaded from the `jwkSetUri`.
- If an algorithm of the MAC family is configured instead (regarding configuration: see below) the signature is validated using a pre-shared secret instead (here: the client secret).
- so in neither case the `userInfoUri` is used for token validation. Even the `jwkSetUri` is not necessarily used - it depends on the algorithm.
- however the `userInfoUri` is used by Spring in the OAuth2 case (not OIDC) to load userInfo to enrich the `OAuth2User` with attributes and authorities. GeoServer uses it if `UserInfo` is the selected role source.
- I think this is only affecting the validations which currently do not reflect this. Also, this should be explained in the user guide. I suppose:
- the config UI should be extended to select an algorithm, RS256 by default (see below)
- if the algorithm belongs to the signature family the `jwkSetUri` is mandatory, otherwise it is optional
- if `UserInfo` is selected as role source the `userInfoUri` is mandatory, otherwise it is optional
- Implement GEOS-11635: _"Add support for opaque auth tokens in OpenID Connect"_.
- I suppose this works out of the box for login. Introspection endpoint is not used by Spring in case of OAuth2 login, but some support is contained in the resource-server Spring lib. Maybe the GEOS-11635 adressed the Resource Server use case, which is not supported here?
### Medium Relevance
- Verify compatibility with Keycloak and GeoNode
- if they were using GS as "Resource Server" (i.e., "OpenID Connect With Attached Access Bearer Tokens") this is not supported anymore, as mentioned above. Consider using the respective extension instead (~ `gs-sec-jwt`).
- Create integration tests:
- Consider using [Spring Authorization Server](https://spring.io/projects/spring-authorization-server) as an OIDC provider. While simple to use, it requires newer Spring versions, making the setup currently more complex.
- An example setup is available in the [gs-sec-oidc-integration-tests repository](https://github.com/awaterme/gs-sec-oidc-integration-tests). This setup is also useful as a lightweight development OIDC provider.
### Lower Relevance
- Improve unit test coverage
- Some validators have been removed:
- `AudienceAccessTokenValidator`: Likely replaced by `OidcIdTokenValidator`.
- `SubjectTokenValidator`: Previously used only for the Resource Server use case?
- Consider implementing Andreas suggestion for a dropdown in the UI to "add provider xy." However, in the current implementation deactivated providers are hidden, reducing the relevance of this feature.
## Compatibility with Prior Implementation
- **Role Sources:**
- Previous implementations may have supported nested JSON paths. Its unclear if this was intentional or necessary.
- **Token Validation:**
- Previously, invalid signatures may have been accepted if "enforce token validation" was set to `false`.
- Now, invalid signatures are always rejected (a reasonable change).
- "Enforce token validation" now only tolerates invalid claims.
- Other potential differences in behavior may exist.
## Next Steps
1. Conduct a thorough review.
- Note: As mentioned earlier, I am not a trained security specialist.
## Future Ideas
- Introduce additional OIDC protocol configurations, such as:
- Allowing users to specify the `JwsAlgorithm` (this has been newly introduced and is now part of the configuration but currently without a UI counterpart).
- I wonder if using the wrong algorithm in the past led to the requirement to make "force token validation" optional.
- Improve parsing of the `.wellknown-operations` endpoint:
- The `JwsAlgorithm` and other details might be automatically detectable.
- Provide an "Apply Preset" UI action for certain identity providers (e.g., ADFS), which pre-fills settings for those providers.
- Document available claims and their semantics for each identity provider as thoroughly as possible.

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ (c) 2018 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.geoserver.community.security</groupId>
<artifactId>gs-sec-oidc</artifactId>
<version>2.28-SNAPSHOT</version>
</parent>
<artifactId>gs-sec-oidc-assembly</artifactId>
<packaging>jar</packaging>
<name>GeoServer OpenID Connect Security Module - Assembly</name>
<dependencies>
<dependency>
<groupId>org.geoserver.community.security</groupId>
<artifactId>gs-sec-oidc-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.geoserver.community.security</groupId>
<artifactId>gs-sec-oidc-web</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,37 @@
<assembly>
<id>sec-oidc-plugin</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>target/dependency</directory>
<outputDirectory></outputDirectory>
<includes>
<include>gs-sec-oidc-core*</include>
<include>gs-sec-oidc-web*</include>
<include>spring-security-core-*</include>
<include>spring-security-config-*</include>
<include>spring-security-crypto*</include>
<include>spring-security-web-*</include>
<include>spring-security-oauth2*</include>
<include>spring-security-jwt*</include>
<include>commons-codec*</include>
<include>jackson-core-*</include>
<include>jackson-databind-*</include>
<include>jackson-annotations-*</include>
<include>jettison-*</include>
<include>json-lib-2.4.2-*</include>
<include>spring-aop-*</include>
<include>spring-context-*</include>
<include>spring-webmvc-*</include>
<include>json-path-*</include>
<include>json-smart*.jar</include>
<include>accessors-smart*</include>
<include>json-simple*</include>
<include>nimbus-jose-jwt*</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ (c) 2018 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.geoserver.community.security</groupId>
<artifactId>gs-sec-oidc</artifactId>
<version>2.28-SNAPSHOT</version>
</parent>
<artifactId>gs-sec-oidc-core</artifactId>
<packaging>jar</packaging>
<name>GeoServer OpenID Connect Security Module - Core</name>
<dependencies>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-main</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.2</version>
</dependency>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-main</artifactId>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-wms</artifactId>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<OPENID_TEST_GS_PROXY_BASE>http://localhost/geoserver</OPENID_TEST_GS_PROXY_BASE>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,87 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import static org.apache.logging.log4j.Level.DEBUG;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.geotools.util.logging.Logging;
/**
* Used to log confidential information, if enabled.
*
* <p>Also adjusts log levels of relevant Spring classes to enable HTTP related logging. Unfortunately it is required to
* use Log4J2 API directly to adjust the log levels at runtime to gain access to the required logger instances.
*
* @author awaterme
*/
public class ConfidentialLogger {
private static final List<String> SPRING_LOGGER_NAMES =
List.of("org.springframework.web.HttpLogging", "org.springframework.security.web.DefaultRedirectStrategy");
private static final Map<String, org.apache.logging.log4j.Level> SPRING_ORG_LEVELS =
determineOrgLevels(SPRING_LOGGER_NAMES);
private static Logger LOGGER = Logging.getLogger(ConfidentialLogger.class);
private static boolean enabled = false;
public static void log(Level level, String msg, Object[] params) {
if (!enabled) {
return;
}
LOGGER.log(level, msg, params);
}
public static boolean isLoggable(Level level) {
return enabled && LOGGER.isLoggable(level);
}
/** @param pEnabled the enabled to set */
public static void setEnabled(boolean pEnabled) {
setSpringLoggersEnabled(pEnabled);
enabled = pEnabled;
}
private static void setSpringLoggersEnabled(boolean pEnabled) {
List<org.apache.logging.log4j.core.Logger> lLoggers = SPRING_LOGGER_NAMES.stream()
.map(n -> LogManager.getLogger(n))
.filter(l -> l instanceof org.apache.logging.log4j.core.Logger)
.map(l -> (org.apache.logging.log4j.core.Logger) l)
.collect(Collectors.toList());
lLoggers.forEach(l -> {
org.apache.logging.log4j.Level lLevel = pEnabled ? DEBUG : SPRING_ORG_LEVELS.get(l.getName());
l.setLevel(lLevel);
});
}
public static Level getLevel() {
return LOGGER.getLevel();
}
/** @return the enabled */
public static boolean isEnabled() {
return enabled;
}
public static void setLevel(Level pLevel) {
LOGGER.setLevel(pLevel);
}
private static Map<String, org.apache.logging.log4j.Level> determineOrgLevels(List<String> pLoggerNames) {
Map<String, org.apache.logging.log4j.Level> lMap = new HashMap<>();
pLoggerNames.forEach(n -> {
org.apache.logging.log4j.Logger lLogger = LogManager.getLogger(n);
lMap.put(n, lLogger.getLevel());
});
return lMap;
}
}

View File

@ -0,0 +1,66 @@
/* (c) 2023 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import org.geoserver.security.validation.FilterConfigException;
public class GeoServerOAuth2FilterConfigException extends FilterConfigException {
/** serialVersionUID */
private static final long serialVersionUID = -3686715589371356406L;
public GeoServerOAuth2FilterConfigException(String errorId, Object... args) {
super(errorId, args);
}
public GeoServerOAuth2FilterConfigException(String errorId, String message, Object... args) {
super(errorId, message, args);
}
public static final String OAUTH2_WKTS_URL_MALFORMED = "OAUTH2_WKTS_URL_MALFORMED";
public static final String OAUTH2_CHECKTOKEN_OR_WKTS_ENDPOINT_URL_REQUIRED =
"OAUTH2_CHECKTOKEN_OR_WKTS_ENDPOINT_URL_REQUIRED";
public static final String OAUTH2_SCOPE_DELIMITER_MIXED = "OAUTH2_SCOPE_DELIMITER_MIXED";
public static final String OAUTH2_CHECKTOKENENDPOINT_URL_REQUIRED = "OAUTH2_CHECKTOKENENDPOINT_URL_REQUIRED";
public static final String OAUTH2_CHECKTOKENENDPOINT_URL_MALFORMED = "OAUTH2_CHECKTOKENENDPOINT_URL_MALFORMED";
public static final String OAUTH2_URL_IN_LOGOUT_URI_MALFORMED = "OAUTH2_URL_IN_LOGOUT_URI_MALFORMED";
public static final String OAUTH2_ACCESSTOKENURI_MALFORMED = "OAUTH2_ACCESSTOKENURI_MALFORMED";
public static final String OAUTH2_ACCESSTOKENURI_NOT_HTTPS = "OAUTH2_ACCESSTOKENURI_NOT_HTTPS";
public static final String OAUTH2_USERAUTHURI_MALFORMED = "OAUTH2_USERAUTHURI_MALFORMED";
public static final String OAUTH2_USERAUTHURI_NOT_HTTPS = "OAUTH2_USERAUTHURI_NOT_HTTPS";
public static final String OAUTH2_REDIRECT_URI_MALFORMED = "OAUTH2_REDIRECT_URI_MALFORMED";
public static final String OAUTH2_CLIENT_ID_REQUIRED = "OAUTH2_CLIENT_ID_REQUIRED";
public static final String OAUTH2_CLIENT_USER_NAME_REQUIRED = "OAUTH2_CLIENT_USER_NAME_REQUIRED";
public static final String OAUTH2_CLIENT_SECRET_REQUIRED = "OAUTH2_CLIENT_SECRET_REQUIRED";
public static final String OAUTH2_SCOPE_REQUIRED = "OAUTH2_SCOPE_REQUIRED";
public static final String OAUTH2_URI_REQUIRED = "OAUTH2_URI_REQUIRED";
public static final String OAUTH2_URI_INVALID = "OAUTH2_URI_INVALID";
public static final String AEP_DENIED_WRONG_PROVIDER_COUNT = "AEP_DENIED_WRONG_PROVIDER_COUNT";
public static final String MSGRAPH_COMBINATION_INVALID = "MSGRAPH_COMBINATION_INVALID";
public static final String ROLE_SOURCE_ID_TOKEN_INVALID_FOR_GITHUB = "ROLE_SOURCE_ID_TOKEN_INVALID_FOR_GITHUB";
public static final String OAUTH2_USER_INFO_URI_REQUIRED_NO_OIDC = "OAUTH2_USER_INFO_URI_REQUIRED_NO_OIDC";
public static final String ROLE_SOURCE_USER_INFO_URI_REQUIRED = "ROLE_SOURCE_USER_INFO_URI_REQUIRED";
public static final String OAUTH2_JWK_SET_URI_REQUIRED = "OAUTH2_JWK_SET_URI_REQUIRED";
}

View File

@ -0,0 +1,292 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import static java.lang.String.format;
import static java.util.logging.Level.SEVERE;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.geoserver.security.filter.GeoServerRoleResolvers.PRE_AUTH_ROLE_SOURCE_RESOLVER;
import static org.geoserver.security.impl.GeoServerUser.ADMIN_USERNAME;
import static org.geoserver.security.impl.GeoServerUser.ROOT_USERNAME;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_MICROSOFT;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverContext;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverParam;
import org.geoserver.security.filter.GeoServerRoleResolvers.RoleResolver;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.impl.RoleCalculator;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource;
import org.geotools.util.logging.Logging;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
/**
* Resolves roles for a given user during login with OAuth2 and OpenID Connect.
*
* @author awaterme
*/
public class GeoServerOAuth2RoleResolver implements RoleResolver {
private static final Logger LOGGER = Logging.getLogger(GeoServerOAuth2RoleResolver.class);
public static final class OAuth2ResolverParam extends ResolverParam {
private OAuth2UserRequest userRequest;
public OAuth2ResolverParam(
String pPrincipal,
HttpServletRequest pRequest,
ResolverContext pContext,
OAuth2UserRequest pUserRequest) {
super(pPrincipal, pRequest, pContext);
userRequest = pUserRequest;
}
/** @return the userRequest */
public OAuth2UserRequest getUserRequest() {
return userRequest;
}
}
/** Default {@link Supplier} just creates a new {@link OAuth2UserService}. */
private Supplier<OAuth2UserService<OAuth2UserRequest, OAuth2User>> userServiceSupplier =
() -> new DefaultOAuth2UserService();
/** Default {@link Supplier} just creates a new MSGraphRolesResolver. */
private Supplier<MSGraphRolesResolver> msGraphRolesResolverSupplier = () -> new MSGraphRolesResolver();
private GeoServerOAuth2LoginFilterConfig config;
/** @param pConfig */
public GeoServerOAuth2RoleResolver(GeoServerOAuth2LoginFilterConfig pConfig) {
super();
config = pConfig;
}
@Override
public Collection<GeoServerRole> convert(ResolverParam pParam) {
if (!(pParam instanceof OAuth2ResolverParam)) {
throw new IllegalArgumentException(OAuth2ResolverParam.class.getSimpleName() + " required");
}
Collection<GeoServerRole> result = new ArrayList<>();
String lPrincipal = pParam.getPrincipal();
if (ADMIN_USERNAME.equalsIgnoreCase(lPrincipal) || ROOT_USERNAME.equalsIgnoreCase(lPrincipal)) {
// avoid unintentional match with pre-existing administrator
String lMsg = "Potentially harmful OAuth2 user '%s' detected. Granting no roles.";
LOGGER.log(Level.WARNING, format(lMsg, lPrincipal));
return result;
}
RoleSource rs = pParam.getContext().getRoleSource();
if (rs == null) {
LOGGER.log(SEVERE, "Role assignment failed. Role source unspecified.");
} else if (rs instanceof OpenIdRoleSource) {
OpenIdRoleSource oirs = (OpenIdRoleSource) rs;
switch (oirs) {
case AccessToken:
result = getRolesFromAccessToken(pParam);
break;
case IdToken:
result = getRolesFromIdToken(pParam);
break;
case UserInfo:
result = getRolesFromUserInfo(pParam);
break;
case MSGraphAPI:
result = getRolesFromMSGraphAPI(pParam);
break;
default:
String lMsg = "Role assigment failed. Unknown roleSource: {0}";
LOGGER.log(SEVERE, lMsg, oirs);
}
} else {
result = PRE_AUTH_ROLE_SOURCE_RESOLVER.convert(pParam);
}
if (result == null) {
result = new ArrayList<>();
}
GeoServerSecurityManager lSecurityManager = pParam.getSecurityManager();
RoleCalculator calc = new RoleCalculator(lSecurityManager.getActiveRoleService());
try {
calc.addInheritedRoles(result);
} catch (IOException e) {
String lMsg = "Role calculation failed on inherited roles for user '%s'.";
LOGGER.log(SEVERE, format(lMsg, pParam.getPrincipal()), e);
}
calc.addMappedSystemRoles(result);
if (!result.contains(GeoServerRole.AUTHENTICATED_ROLE)) {
result.add(GeoServerRole.AUTHENTICATED_ROLE);
}
if (LOGGER.isLoggable(Level.FINE)) {
String lUser = lPrincipal;
String lSrc = rs == null ? null : rs.toString();
String lRoles = result.stream().map(r -> r.getAuthority()).collect(joining(","));
LOGGER.fine(format("User '%s' received roles from roleSource=%s: %s", lUser, lSrc, lRoles));
}
return result;
}
private Collection<GeoServerRole> getRolesFromAccessToken(ResolverParam pParam) {
OAuth2UserRequest lUsrRequest = ((OAuth2ResolverParam) pParam).getUserRequest();
OAuth2AccessToken lAccessToken = lUsrRequest.getAccessToken();
String lClaimName = config.getTokenRolesClaim();
Collection<String> lRoles = new ArrayList<>();
Set<String> lScopes = lAccessToken.getScopes();
if (LOGGER.isLoggable(Level.FINE)) {
String lMsg = "Analyzing access token for roles. Claim: %s. Scopes: %s, additionals: %s";
String lScopeTxt = lScopes == null ? null : lScopes.stream().collect(joining(","));
LOGGER.fine(format(lMsg, lClaimName, lScopeTxt, lUsrRequest.getAdditionalParameters()));
}
if ("scope".equals(lClaimName)) {
lRoles = lAccessToken.getScopes();
} else {
Object lObject = lUsrRequest.getAdditionalParameters().get(lClaimName);
lRoles = toStringList(lObject, pParam);
}
return lRoles.stream().map(r -> new GeoServerRole(r)).collect(toList());
}
private Collection<GeoServerRole> getRolesFromIdToken(ResolverParam pParam) {
OAuth2UserRequest lUsrRequest = ((OAuth2ResolverParam) pParam).getUserRequest();
OidcUserRequest lOidcReq = lUsrRequest instanceof OidcUserRequest ? ((OidcUserRequest) lUsrRequest) : null;
if (lOidcReq == null) {
String lMsg = "Role extraction failed. ID token unavailable for clientRegistration %s.";
LOGGER.log(SEVERE, format(lMsg, lUsrRequest.getClientRegistration().getRegistrationId()));
return new ArrayList<>();
}
String lClaimName = config.getTokenRolesClaim();
Collection<String> lRoles = new ArrayList<>();
OidcIdToken lIdToken = lOidcReq.getIdToken();
if (LOGGER.isLoggable(Level.FINE)) {
String lMsg = "Analyzing access token for roles. Claim: %s. Claims: %s";
LOGGER.fine(format(lMsg, lClaimName, lIdToken.getClaims()));
}
List<String> lClaimList = lIdToken.getClaimAsStringList(lClaimName);
if (lClaimList != null) {
lRoles.addAll(lClaimList);
}
return lRoles.stream().map(r -> new GeoServerRole(r)).collect(toList());
}
private Collection<GeoServerRole> getRolesFromUserInfo(ResolverParam pParam) {
OAuth2UserRequest lUsrRequest = ((OAuth2ResolverParam) pParam).getUserRequest();
OAuth2UserService<OAuth2UserRequest, OAuth2User> lService = userServiceSupplier.get();
OAuth2User lUser = lService.loadUser(lUsrRequest);
String lClaimName = config.getTokenRolesClaim();
Collection<String> lRoles = new ArrayList<>();
if (LOGGER.isLoggable(Level.FINE)) {
String lMsg = "Analyzing userInfo for roles. Claim: %s. User: %s";
LOGGER.fine(format(lMsg, lClaimName, lUser));
}
if ("authorities".equals(lClaimName)) {
Collection<? extends GrantedAuthority> authorities = lUser.getAuthorities();
lRoles = authorities.stream().map(a -> a.getAuthority()).collect(toList());
} else {
Object lObject = lUser.getAttribute(lClaimName);
lRoles = toStringList(lObject, pParam);
}
return lRoles.stream().map(r -> new GeoServerRole(r)).collect(toList());
}
private Collection<GeoServerRole> getRolesFromMSGraphAPI(ResolverParam pParam) {
OAuth2UserRequest lUsrRequest = ((OAuth2ResolverParam) pParam).getUserRequest();
ClientRegistration lClientReg = lUsrRequest.getClientRegistration();
String lUsr = pParam.getPrincipal();
if (!REG_ID_MICROSOFT.equals(lClientReg.getRegistrationId())) {
// actually prevented by UI validation, but make sure here to not send foreign access
// token around
String lMsg = "Resolving roles failed. RoleSource Microsoft Graph API supported with "
+ "provider %s only. Currently processing login with %s instead.";
LOGGER.log(SEVERE, format(lMsg, REG_ID_MICROSOFT, lClientReg.getClientName()));
return new ArrayList<>();
}
Collection<String> lRoles = new ArrayList<>();
try {
String accessToken = lUsrRequest.getAccessToken().getTokenValue();
MSGraphRolesResolver resolver = msGraphRolesResolverSupplier.get();
lRoles = resolver.resolveRoles(accessToken);
if (LOGGER.isLoggable(Level.FINE)) {
String lMsg = "Role assignments for '%s' from MS Graph: %s";
String lRolesTxt = lRoles.stream().collect(joining(","));
LOGGER.fine(format(lMsg, lUsr, lRolesTxt));
}
} catch (IOException e) {
String lMsg = "Resolving roles from Microsoft Graph API failed for user '%s'.";
LOGGER.log(SEVERE, format(lMsg, lUsr), e);
}
return lRoles.stream().map(r -> new GeoServerRole(r)).collect(toList());
}
private Collection<String> toStringList(Object pObject, ResolverParam pParam) {
if (pObject == null) {
String lMsg = "Role extraction failed. User '%s', roleSource=%s: Claim '%s' is missing.";
String lClaim = config.getTokenRolesClaim();
LOGGER.log(SEVERE, format(lMsg, pParam.getPrincipal(), pParam.getRoleSource(), lClaim));
return new ArrayList<>();
} else if (pObject instanceof String) {
return Collections.singleton(pObject.toString());
} else if (pObject instanceof String[]) {
return Arrays.asList((String[]) pObject);
} else if (pObject instanceof List) {
List<?> lList = (List<?>) pObject;
List<String> lRoles = lList.stream()
.filter(o -> o instanceof String)
.map(o -> (String) o)
.collect(toList());
if (lRoles.size() == lList.size()) {
// only consider if all strings
return lRoles;
}
}
String lUser = pParam.getPrincipal();
String lType = pObject.getClass().getName();
String lValue = pObject.toString();
String lMsg = "Role extraction failed. User '%s', roleSource=%s: Type %s is not supported.";
lMsg += " Value: %s";
LOGGER.log(SEVERE, format(lMsg, lUser, pParam.getRoleSource(), lType, lValue));
return new ArrayList<>();
}
public void setUserServiceSupplier(
Supplier<OAuth2UserService<OAuth2UserRequest, OAuth2User>> pUserServiceSupplier) {
if (pUserServiceSupplier == null) {
throw new IllegalArgumentException("Supplier for OAuth2UserService must not be null.");
}
this.userServiceSupplier = pUserServiceSupplier;
}
public void setMsGraphRolesResolverSupplier(Supplier<MSGraphRolesResolver> pMsGraphRolesResolverSupplier) {
if (pMsGraphRolesResolverSupplier == null) {
throw new IllegalArgumentException("Supplier for MSGraphRolesResolver must not be null.");
}
this.msGraphRolesResolverSupplier = pMsGraphRolesResolverSupplier;
}
}

View File

@ -0,0 +1,147 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import java.util.Collection;
import java.util.function.Supplier;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.security.filter.GeoServerRoleResolvers;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverContext;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverParam;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.oauth2.common.GeoServerOAuth2RoleResolver.OAuth2ResolverParam;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geotools.util.logging.Logging;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
/**
* Provides {@link OAuth2UserService} implementation for OAuth2 and OpenID Connect. Allows for integration with the
* GeoServer supported user role sources.
*
* @author awaterme
*/
public class GeoServerOAuth2UserServices {
private static final Logger LOGGER = Logging.getLogger(GeoServerOAuth2UserServices.class);
static class GeoServerOAuth2UserService extends GeoServerOAuth2UserServices
implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private Supplier<OAuth2UserService<OAuth2UserRequest, OAuth2User>> delegateSupplier =
() -> new DefaultOAuth2UserService();
public GeoServerOAuth2UserService(
ResolverContext resolverContext,
Supplier<HttpServletRequest> requestSupplier,
GeoServerOAuth2LoginFilterConfig config) {
super(resolverContext, requestSupplier, config);
}
@Override
public OAuth2User loadUser(OAuth2UserRequest pUserRequest) throws OAuth2AuthenticationException {
OAuth2User lUser = delegateSupplier.get().loadUser(pUserRequest);
String lUserName = lUser.getName();
String lUserNameAttributeName = userNameAttributeName(pUserRequest);
Collection<GeoServerRole> roles = determineRoles(lUserName, pUserRequest);
return new DefaultOAuth2User(roles, lUser.getAttributes(), lUserNameAttributeName);
}
public void setDelegateSupplier(Supplier<OAuth2UserService<OAuth2UserRequest, OAuth2User>> delegateSupplier) {
this.delegateSupplier = delegateSupplier;
}
}
static class GeoServerOidcUserService extends GeoServerOAuth2UserServices
implements OAuth2UserService<OidcUserRequest, OidcUser> {
private Supplier<OAuth2UserService<OidcUserRequest, OidcUser>> delegateSupplier = () -> new OidcUserService();
public GeoServerOidcUserService(
ResolverContext resolverContext,
Supplier<HttpServletRequest> requestSupplier,
GeoServerOAuth2LoginFilterConfig config) {
super(resolverContext, requestSupplier, config);
}
@Override
public OidcUser loadUser(OidcUserRequest pUserRequest) throws OAuth2AuthenticationException {
OidcUser lUser = delegateSupplier.get().loadUser(pUserRequest);
String lUserName = lUser.getName();
String lUserNameAttributeName = userNameAttributeName(pUserRequest);
Collection<GeoServerRole> roles = determineRoles(lUserName, pUserRequest);
return new DefaultOidcUser(roles, lUser.getIdToken(), lUser.getUserInfo(), lUserNameAttributeName);
}
public void setDelegateSupplier(Supplier<OAuth2UserService<OidcUserRequest, OidcUser>> delegateSupplier) {
this.delegateSupplier = delegateSupplier;
}
}
public static OAuth2UserService<OidcUserRequest, OidcUser> newOidcUserService(
GeoServerRoleResolvers.ResolverContext pResolverContext,
Supplier<HttpServletRequest> pReqSupplier,
GeoServerOAuth2LoginFilterConfig pConfig) {
GeoServerOidcUserService lService = new GeoServerOidcUserService(pResolverContext, pReqSupplier, pConfig);
return lService;
}
public static OAuth2UserService<OAuth2UserRequest, OAuth2User> newOAuth2UserService(
GeoServerRoleResolvers.ResolverContext pResolverContext,
Supplier<HttpServletRequest> pReqSupplier,
GeoServerOAuth2LoginFilterConfig pConfig) {
GeoServerOAuth2UserService lService = new GeoServerOAuth2UserService(pResolverContext, pReqSupplier, pConfig);
return lService;
}
protected GeoServerRoleResolvers.ResolverContext resolverContext;
protected Supplier<HttpServletRequest> requestSupplier;
protected GeoServerOAuth2LoginFilterConfig config;
protected Supplier<GeoServerOAuth2RoleResolver> resolverSupplier = () -> new GeoServerOAuth2RoleResolver(config);
public GeoServerOAuth2UserServices(
ResolverContext resolverContext,
Supplier<HttpServletRequest> requestSupplier,
GeoServerOAuth2LoginFilterConfig config) {
super();
this.resolverContext = resolverContext;
this.requestSupplier = requestSupplier;
this.config = config;
}
protected String userNameAttributeName(OAuth2UserRequest pUserRequest) {
// null check performed by delegate already
String lUserNameAttributeName = pUserRequest
.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
return lUserNameAttributeName;
}
protected Collection<GeoServerRole> determineRoles(String pUserName, OAuth2UserRequest pRequest) {
LOGGER.fine("Resolving roles for user '" + pUserName + "'.");
HttpServletRequest lRequest = requestSupplier.get();
ResolverParam lParam = new OAuth2ResolverParam(pUserName, lRequest, resolverContext, pRequest);
GeoServerOAuth2RoleResolver lResolver = resolverSupplier.get();
Collection<GeoServerRole> roles = lResolver.convert(lParam);
return roles;
}
public void setResolverSupplier(Supplier<GeoServerOAuth2RoleResolver> resolverSupplier) {
this.resolverSupplier = resolverSupplier;
}
}

View File

@ -0,0 +1,28 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Provides access to the current {@link HttpServletRequest}.
*
* @author awaterme
*/
public class HttpServletRequestSupplier implements Supplier<HttpServletRequest> {
@Override
public HttpServletRequest get() {
ServletRequestAttributes lAttrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (lAttrs == null) {
throw new IllegalStateException("Failed to obtain ServletRequestAttributes.");
}
HttpServletRequest lRequest = lAttrs.getRequest();
return lRequest;
}
}

View File

@ -0,0 +1,33 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
/**
* Capable of turning JwsAlgorithm names into {@link JwsAlgorithm} objects.
*
* @author awaterme
*/
public class JwsAlgorithmNameParser {
/**
* @param pString
* @return the algorithm or null if no match was found
*/
public JwsAlgorithm parse(String pString) {
if (pString == null || pString.isBlank()) {
return null;
}
SignatureAlgorithm lAlg = SignatureAlgorithm.from(pString);
if (lAlg != null) {
return lAlg;
}
MacAlgorithm lMac = MacAlgorithm.from(pString);
return lMac;
}
}

View File

@ -0,0 +1,110 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.geotools.util.logging.Logging;
/**
* Verify role using Azure graph.
*
* <p>Make sure your Azure AD application has "GroupMember.Read.All" permission: a) go to your application in Azure AD
* (in the portal) b) On the left, go to "API permissions" c) click "Add a permission" d) press "Microsoft Graph" e)
* press "Delegated permission" f) Scroll down to "GroupMember" g) Choose "GroupMemeber.Read.All" h) press "Add
* permission" i) on the API Permission screen, press the "Grant admin consent for ..." text
*
* <p>This class will go to the "https://graph.microsoft.com/v1.0/me/memberOf" and attach your access token. It will
* then read the response and find all the user's groups.
*
* <p>NOTE: to be consistent with the rest of Azure, we use the Groups OID (guid) NOT its name.
*/
public class MSGraphRolesResolver {
private static final Logger LOGGER = Logging.getLogger(MSGraphRolesResolver.class);
static URL memberOfEndpoint;
static {
try {
memberOfEndpoint = new URL("https://graph.microsoft.com/v1.0/me/memberOf");
} catch (MalformedURLException e) {
// this shouldn't happen (unless typo in above line)
LOGGER.log(Level.WARNING, "Error parsing MS GRAPH API URL", e);
}
}
// for testing, we make this package readable
String authorizationHeaderName = "Authorization";
public HttpURLConnection createHTTPRequest(String accessToken) throws IOException {
String tokenHeaderValue = "Bearer " + accessToken;
HttpURLConnection http = (HttpURLConnection) memberOfEndpoint.openConnection();
http.setRequestProperty("Accept", "application/json");
http.setRequestProperty(authorizationHeaderName, tokenHeaderValue);
return http;
}
/**
* talk to the actual azure graph api to get user's group memberships. 1. attaches the access token to the request.
* 2. sets the Accepts header to "application/json" (required)
*
* @param accessToken
* @return
* @throws IOException
*/
private String resolveUrl(String accessToken) throws IOException {
HttpURLConnection http = createHTTPRequest(accessToken);
try (BufferedReader lReader = new BufferedReader(new InputStreamReader(http.getInputStream()))) {
String result = lReader.lines().collect(Collectors.joining("\n"));
return result;
} finally {
http.disconnect();
}
}
// parses the resulting json from the user's group memberships json result.
// returns a list of the groups (object id) that the user is a member of.
public List<String> parseJson(String jsonString) throws JSONException {
List<String> result = new ArrayList<>();
JSONObject json = JSONObject.fromObject(jsonString);
JSONArray values = json.getJSONArray("value");
for (Object value : values) {
JSONObject object = (JSONObject) value;
if (!object.get("@odata.type").equals("#microsoft.graph.group")) continue;
result.add(object.get("id").toString());
}
return result;
}
/**
* call the MS Graph API and get a list of object ids (guid strings) - one for each group the user is a member of.
*
* @param accessToken - access token (from MS azure ad)
* @return list of groups (guid strings) the user is a member of
* @throws IOException
*/
public List<String> resolveRoles(String accessToken) throws IOException {
String jsonStr = resolveUrl(accessToken);
List<String> result = parseJson(jsonStr);
return result;
}
}

View File

@ -0,0 +1,18 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
/**
* Defines IDs for the supported Spring OAuth2 ClientRegistrations.
*
* @author awaterme
*/
public interface GeoServerOAuth2ClientRegistrationId {
String REG_ID_GIT_HUB = "gitHub";
String REG_ID_GOOGLE = "google";
String REG_ID_OIDC = "oidc";
String REG_ID_MICROSOFT = "microsoft";
}

View File

@ -0,0 +1,87 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.geoserver.security.filter.GeoServerAuthenticationFilter;
import org.geoserver.security.filter.GeoServerCompositeFilter;
import org.geotools.util.logging.Logging;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
/**
* {@link Filter} supports OpenID Connect and OAuth2 based logins by delegating to the nested Spring filter
* implementations.
*
* <p>The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by
* using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google).
* OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". OAuth 2.0 Login is implemented
* by using the Authorization Code Grant, as specified in the OAuth 2.0 Authorization Framework and OpenID Connect Core
* 1.0.
*
* <p>Documentation: Diagrams exist in gs-sec-oidc/doc/diagrams, showing how to pieces belong together.
*
* <p>Spring OAuth2 feature matrix: https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix
*
* @see GeoServerOAuth2LoginAuthenticationProvider containing the setup
* @author awaterme
*/
public class GeoServerOAuth2LoginAuthenticationFilter extends GeoServerCompositeFilter
implements GeoServerAuthenticationFilter, LogoutHandler {
private static final Logger LOGGER = Logging.getLogger(GeoServerOAuth2LoginAuthenticationFilter.class);
private LogoutSuccessHandler logoutSuccessHandler;
public GeoServerOAuth2LoginAuthenticationFilter() {
super();
}
@Override
public boolean applicableForHtml() {
return true;
}
@Override
public boolean applicableForServices() {
return true;
}
@Override
public void logout(HttpServletRequest pRequest, HttpServletResponse pResponse, Authentication pAuthentication) {
// Note: The spring handler for logout is by design a logout *success* handler rather than
// a logout handler. Here it is treated as one of potentially many GS logoutHandlers.
// Reason: GeoServers logout handler determination is not so flexible yet. However GS
// OIDC logout both work, so this seems acceptable for now. The actual GS
// logoutSuccessHandler tolerates that something else might have committed the response
// already.
if (logoutSuccessHandler == null) {
return;
}
try {
logoutSuccessHandler.onLogoutSuccess(pRequest, pResponse, pAuthentication);
} catch (IOException | ServletException e) {
LOGGER.log(Level.SEVERE, "Logout from OAuth2/OIDC provider failed.", e);
}
}
/** @param pLogoutSuccessHandler the logoutSuccessHandler to set */
public void setLogoutSuccessHandler(LogoutSuccessHandler pLogoutSuccessHandler) {
logoutSuccessHandler = pLogoutSuccessHandler;
}
/** @return the logoutSuccessHandler */
public LogoutSuccessHandler getLogoutSuccessHandler() {
return logoutSuccessHandler;
}
}

View File

@ -0,0 +1,550 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toList;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2UserServices.newOAuth2UserService;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2UserServices.newOidcUserService;
import static org.geoserver.security.oauth2.login.OAuth2LoginButtonEnablementEvent.disableButtonEvent;
import static org.geoserver.security.oauth2.login.OAuth2LoginButtonEnablementEvent.enableButtonEvent;
import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.security.GeoServerRoleConverter;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource;
import org.geoserver.security.filter.GeoServerRoleResolvers;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverContext;
import org.geoserver.security.oauth2.common.ConfidentialLogger;
import org.geoserver.security.oauth2.common.HttpServletRequestSupplier;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginCustomizers.ClientRegistrationCustomizer;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginCustomizers.HttpSecurityCustomizer;
import org.geoserver.security.oauth2.spring.GeoServerAuthorizationRequestCustomizer;
import org.geoserver.security.oauth2.spring.GeoServerOAuth2AccessTokenResponseClient;
import org.geoserver.security.oauth2.spring.GeoServerOidcIdTokenDecoderFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.RequestMatcherRedirectFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Builder for {@link GeoServerOAuth2LoginAuthenticationFilter}.
*
* <p>Further documentation: Look at {@link GeoServerOAuth2LoginAuthenticationFilter}
*
* @see GeoServerOAuth2LoginAuthenticationFilter
* @author awaterme
*/
public class GeoServerOAuth2LoginAuthenticationFilterBuilder implements GeoServerOAuth2ClientRegistrationId {
/** Filter types required for GeoServer */
private static final List<Class<?>> REQ_FILTER_TYPES = asList(
OAuth2AuthorizationRequestRedirectFilter.class,
OAuth2LoginAuthenticationFilter.class,
RequestCacheAwareFilter.class);
// mandatory
private GeoServerOAuth2LoginFilterConfig configuration;
private GeoServerSecurityManager securityManager;
private HttpSecurity http;
private ApplicationEventPublisher eventPublisher;
private GeoServerOidcIdTokenDecoderFactory tokenDecoderFactory;
private InMemoryClientRegistrationRepository clientRegistrationRepository;
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService;
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
private OAuth2AuthorizedClientService authorizedClientService;
private OAuth2AuthorizedClientRepository authorizedClientRepository;
private DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver;
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private LogoutSuccessHandler logoutSuccessHandler;
private Filter redirectToProviderFilter;
private Supplier<HttpServletRequest> requestSupplier;
private GeoServerRoleResolvers.ResolverContext roleResolverContext;
private boolean closed;
// Might be used for customizations.
private HttpSecurityCustomizer httpSecurityCustomizer = (h) -> {};
private ClientRegistrationCustomizer clientRegistrationCustomizer = (h) -> {};
public GeoServerOAuth2LoginAuthenticationFilterBuilder() {
super();
}
/**
* Builds a new filter, setup with the given configuration. Must be called once only.
*
* @return a new filter
*/
public GeoServerOAuth2LoginAuthenticationFilter build() {
validate();
GeoServerOAuth2LoginAuthenticationFilter filter = new GeoServerOAuth2LoginAuthenticationFilter();
if (0 < configuration.getActiveProviderCount()) {
filter.setLogoutSuccessHandler(getLogoutSuccessHandler());
List<Filter> lFilters = createNestedFilters();
filter.setNestedFilters(lFilters);
}
ConfidentialLogger.setEnabled(configuration.isOidcAllowUnSecureLogging());
return filter;
}
private void validate() {
Assert.notNull(configuration, "Property 'configuration' must not be null");
Assert.notNull(http, "Property 'http' must not be null");
Assert.notNull(securityManager, "Property 'securityManager' must not be null");
Assert.notNull(eventPublisher, "Property 'eventPublisher' must not be null");
Assert.notNull(tokenDecoderFactory, "Property 'tokenDecoderFactory' must not be null");
Assert.isTrue(!closed, "Builder must not be reused.");
closed = true;
}
private List<Filter> createNestedFilters() {
try {
return createFiltersImpl();
} catch (Exception e) {
throw new RuntimeException("Failed to create filter.", e);
}
}
private List<Filter> createFiltersImpl() throws Exception {
// Attention: Singleton (also picked up by Spring) uses this config. If multiple instances
// of this filter shall be allowed in the future (not planned), adjust this accordingly.
tokenDecoderFactory.setGeoServerOAuth2LoginFilterConfig(configuration);
http.oauth2Login(oauthConfig -> {
oauthConfig.clientRegistrationRepository(getClientRegistrationRepository());
oauthConfig.authorizedClientRepository(getAuthorizedClientRepository());
oauthConfig.authorizedClientService(getAuthorizedClientService());
oauthConfig.userInfoEndpoint().userService(getOauth2UserService());
oauthConfig.userInfoEndpoint().oidcUserService(getOidcUserService());
oauthConfig.authorizationEndpoint().authorizationRequestResolver(getAuthorizationRequestResolver());
oauthConfig.tokenEndpoint().accessTokenResponseClient(getAccessTokenResponseClient());
});
httpSecurityCustomizer.accept(http);
SecurityFilterChain lChain = http.build();
List<Filter> lFilters = lChain.getFilters();
lFilters = lFilters.stream()
.filter(f -> REQ_FILTER_TYPES.contains(f.getClass()))
.collect(toList());
String lAuthEntryPoint = configuration.getAuthenticationEntryPointRedirectUri();
if (configuration.getEnableRedirectAuthenticationEntryPoint() && lAuthEntryPoint != null) {
lFilters.add(getRedirectToProviderFilter());
}
return lFilters;
}
private ResolverContext createRoleResolverContext() {
GeoServerRoleConverter lConverter = null;
if (PreAuthenticatedUserNameRoleSource.Header.equals(configuration.getRoleSource())) {
String converterName = configuration.getRoleConverterName();
lConverter = GeoServerRoleResolvers.loadConverter(converterName);
}
return new GeoServerRoleResolvers.DefaultResolverContext(
securityManager,
configuration.getRoleServiceName(),
configuration.getUserGroupServiceName(),
configuration.getRolesHeaderAttribute(),
lConverter,
configuration.getRoleSource());
}
private InMemoryClientRegistrationRepository createClientRegistrationRepository() {
List<ClientRegistration> lRegistrations = new ArrayList<>();
if (configuration.isGoogleEnabled()) {
lRegistrations.add(createGoogleClientRegistration());
eventPublisher.publishEvent(enableButtonEvent(this, REG_ID_GOOGLE));
} else {
eventPublisher.publishEvent(disableButtonEvent(this, REG_ID_GOOGLE));
}
if (configuration.isGitHubEnabled()) {
lRegistrations.add(createGitHubClientRegistration());
eventPublisher.publishEvent(enableButtonEvent(this, REG_ID_GIT_HUB));
} else {
eventPublisher.publishEvent(disableButtonEvent(this, REG_ID_GIT_HUB));
}
if (configuration.isMsEnabled()) {
lRegistrations.add(createMicrosoftClientRegistration());
eventPublisher.publishEvent(enableButtonEvent(this, REG_ID_MICROSOFT));
} else {
eventPublisher.publishEvent(disableButtonEvent(this, REG_ID_MICROSOFT));
}
if (configuration.isOidcEnabled()) {
lRegistrations.add(createCustomProviderRegistration());
eventPublisher.publishEvent(enableButtonEvent(this, REG_ID_OIDC));
} else {
eventPublisher.publishEvent(disableButtonEvent(this, REG_ID_OIDC));
}
return new InMemoryClientRegistrationRepository(lRegistrations);
}
private OAuth2AuthorizedClientService createAuthorizedClientService(
ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(getClientRegistrationRepository());
}
private OAuth2AuthorizedClientRepository createAuthorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
private ClientRegistration createGoogleClientRegistration() {
/*
* Wellknown-endpoint:
* - https://accounts.google.com/.well-known/openid-configuration
* Documentation:
* - https://developers.google.com/identity/openid-connect/openid-connect
* Logout@Google:
* - seems currently not supported
* - https://stackoverflow.com/questions/4202161/google-account-logout-and-redirect
*/
ClientRegistration lReg = CommonOAuth2Provider.GOOGLE
// registrationId is used in paths (login and authorization)
.getBuilder(REG_ID_GOOGLE)
.clientId(configuration.getGoogleClientId())
.clientSecret(configuration.getGoogleClientSecret())
.userNameAttributeName(configuration.getGoogleUserNameAttribute())
.redirectUri(configuration.getGoogleRedirectUri())
.build();
clientRegistrationCustomizer.accept(lReg);
return lReg;
}
private ClientRegistration createGitHubClientRegistration() {
/*
* GitHub does not support OIDC, but OAuth2.
*
* Wellknown-endpoint:
* - n/a
* Documentation:
* - https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
* Further Information:
* - https://stackoverflow.com/questions/71741596/how-do-i-implement-social-login-with-github-accounts
* Logout@GitHub:
* - seems currently not supported
*/
ClientRegistration lReg = CommonOAuth2Provider.GITHUB
// registrationId is used in paths (login and authorization)
.getBuilder(REG_ID_GIT_HUB)
.clientId(configuration.getGitHubClientId())
.clientSecret(configuration.getGitHubClientSecret())
.userNameAttributeName(configuration.getGitHubUserNameAttribute())
.redirectUri(configuration.getGitHubRedirectUri())
.build();
clientRegistrationCustomizer.accept(lReg);
return lReg;
}
private ClientRegistration createMicrosoftClientRegistration() {
/*
* Wellknown-endpoint:
* - https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
*/
String lScopeTxt = configuration.getMsScopes();
String[] lScopes = ScopeUtils.valueOf(lScopeTxt);
ClientRegistration lReg = ClientRegistration
// registrationId is used in paths (login and authorization)
.withRegistrationId(REG_ID_MICROSOFT)
.clientId(configuration.getMsClientId())
.clientSecret(configuration.getMsClientSecret())
.userNameAttributeName(configuration.getMsUserNameAttribute())
.redirectUri(configuration.getMsRedirectUri())
.clientAuthenticationMethod(CLIENT_SECRET_BASIC)
.authorizationGrantType(AUTHORIZATION_CODE)
.scope(lScopes)
.authorizationUri("https://login.microsoftonline.com/common/oauth2/v2.0/authorize")
.tokenUri("https://login.microsoftonline.com/common/oauth2/v2.0/token")
.userInfoUri("https://graph.microsoft.com/oidc/userinfo")
.jwkSetUri("https://login.microsoftonline.com/common/discovery/v2.0/keys")
.providerConfigurationMetadata(singletonMap(
"end_session_endpoint", "https://login.microsoftonline.com/common/oauth2/v2.0/logout"))
.clientName(REG_ID_MICROSOFT)
.build();
clientRegistrationCustomizer.accept(lReg);
return lReg;
}
private ClientRegistration createCustomProviderRegistration() {
String lScopeTxt = configuration.getOidcScopes();
String[] lScopes = ScopeUtils.valueOf(lScopeTxt);
ClientAuthenticationMethod lAuthMethod =
configuration.isOidcAuthenticationMethodPostSecret() ? CLIENT_SECRET_POST : CLIENT_SECRET_BASIC;
ClientRegistration lReg = ClientRegistration
// registrationId is used in paths (login and authorization)
.withRegistrationId(REG_ID_OIDC)
.clientId(configuration.getOidcClientId())
.clientSecret(configuration.getOidcClientSecret())
.userNameAttributeName(configuration.getOidcUserNameAttribute())
.redirectUri(configuration.getOidcRedirectUri())
.clientAuthenticationMethod(lAuthMethod)
.authorizationGrantType(AUTHORIZATION_CODE)
.scope(lScopes)
.authorizationUri(configuration.getOidcAuthorizationUri())
.tokenUri(configuration.getOidcTokenUri())
.userInfoUri(configuration.getOidcUserInfoUri())
.jwkSetUri(configuration.getOidcJwkSetUri())
.providerConfigurationMetadata(singletonMap("end_session_endpoint", configuration.getOidcLogoutUri()))
.clientName(REG_ID_OIDC)
.build();
clientRegistrationCustomizer.accept(lReg);
return lReg;
}
/** @return the logoutSuccessHandler */
public LogoutSuccessHandler getLogoutSuccessHandler() {
if (logoutSuccessHandler == null) {
OidcClientInitiatedLogoutSuccessHandler lLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(getClientRegistrationRepository());
lLogoutSuccessHandler.setPostLogoutRedirectUri(configuration.getPostLogoutRedirectUri());
logoutSuccessHandler = lLogoutSuccessHandler;
}
return logoutSuccessHandler;
}
/** @param pLogoutSuccessHandler the logoutSuccessHandler to set */
public void setLogoutSuccessHandler(LogoutSuccessHandler pLogoutSuccessHandler) {
logoutSuccessHandler = pLogoutSuccessHandler;
}
/** @return the oAuth2UserService */
public OAuth2UserService<OAuth2UserRequest, OAuth2User> getOauth2UserService() {
if (oauth2UserService == null) {
oauth2UserService = newOAuth2UserService(getRoleResolverContext(), getRequestSupplier(), configuration);
}
return oauth2UserService;
}
/** @param pOAuth2UserService the oAuth2UserService to set */
public void setOauth2UserService(OAuth2UserService<OAuth2UserRequest, OAuth2User> pOAuth2UserService) {
oauth2UserService = pOAuth2UserService;
}
/** @return the oidcUserService */
public OAuth2UserService<OidcUserRequest, OidcUser> getOidcUserService() {
if (oidcUserService == null) {
oidcUserService = newOidcUserService(getRoleResolverContext(), getRequestSupplier(), configuration);
}
return oidcUserService;
}
/** @param pOidcUserService the oidcUserService to set */
public void setOidcUserService(OAuth2UserService<OidcUserRequest, OidcUser> pOidcUserService) {
oidcUserService = pOidcUserService;
}
/** @return the roleResolverContext */
public GeoServerRoleResolvers.ResolverContext getRoleResolverContext() {
if (roleResolverContext == null) {
roleResolverContext = createRoleResolverContext();
}
return roleResolverContext;
}
/** @param pRoleResolverContext the roleResolverContext to set */
public void setRoleResolverContext(GeoServerRoleResolvers.ResolverContext pRoleResolverContext) {
roleResolverContext = pRoleResolverContext;
}
/** @return the requestSupplier */
public Supplier<HttpServletRequest> getRequestSupplier() {
if (requestSupplier == null) {
requestSupplier = new HttpServletRequestSupplier();
}
return requestSupplier;
}
/** @param pRequestSupplier the requestSupplier to set */
public void setRequestSupplier(Supplier<HttpServletRequest> pRequestSupplier) {
requestSupplier = pRequestSupplier;
}
/** @return the clientRegistrationRepository */
public InMemoryClientRegistrationRepository getClientRegistrationRepository() {
if (clientRegistrationRepository == null) {
clientRegistrationRepository = createClientRegistrationRepository();
}
return clientRegistrationRepository;
}
/** @param pClientRegistrationRepository the clientRegistrationRepository to set */
public void setClientRegistrationRepository(InMemoryClientRegistrationRepository pClientRegistrationRepository) {
clientRegistrationRepository = pClientRegistrationRepository;
}
/** @return the authorizedClientService */
public OAuth2AuthorizedClientService getAuthorizedClientService() {
if (authorizedClientService == null) {
authorizedClientService = createAuthorizedClientService(getClientRegistrationRepository());
}
return authorizedClientService;
}
/** @param pAuthorizedClientService the authorizedClientService to set */
public void setAuthorizedClientService(OAuth2AuthorizedClientService pAuthorizedClientService) {
authorizedClientService = pAuthorizedClientService;
}
/** @return the authorizedClientRepository */
public OAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
if (authorizedClientRepository == null) {
authorizedClientRepository = createAuthorizedClientRepository(getAuthorizedClientService());
}
return authorizedClientRepository;
}
/** @param pAuthorizedClientRepository the authorizedClientRepository to set */
public void setAuthorizedClientRepository(OAuth2AuthorizedClientRepository pAuthorizedClientRepository) {
authorizedClientRepository = pAuthorizedClientRepository;
}
/** @return the authorizationRequestResolver */
public DefaultOAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() {
if (authorizationRequestResolver == null) {
authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(
getClientRegistrationRepository(), DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
authorizationRequestResolver.setAuthorizationRequestCustomizer(
new GeoServerAuthorizationRequestCustomizer(configuration));
}
return authorizationRequestResolver;
}
/** @param pAuthorizationRequestResolver the authorizationRequestResolver to set */
public void setAuthorizationRequestResolver(
DefaultOAuth2AuthorizationRequestResolver pAuthorizationRequestResolver) {
authorizationRequestResolver = pAuthorizationRequestResolver;
}
/** @return the redirectToProviderFilter */
public Filter getRedirectToProviderFilter() {
if (redirectToProviderFilter == null) {
AuthenticationTrustResolver trust = new AuthenticationTrustResolverImpl();
RequestMatcher lMatcher = r -> {
Authentication lAuth = SecurityContextHolder.getContext().getAuthentication();
if (lAuth == null) {
return true;
}
boolean lFullyAuthenticated = !trust.isAnonymous(lAuth) && !trust.isRememberMe(lAuth);
return !lFullyAuthenticated;
};
redirectToProviderFilter =
new RequestMatcherRedirectFilter(lMatcher, configuration.getAuthenticationEntryPointRedirectUri());
}
return redirectToProviderFilter;
}
/** @return the accessTokenResponseClient */
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> getAccessTokenResponseClient() {
if (accessTokenResponseClient == null) {
accessTokenResponseClient = new GeoServerOAuth2AccessTokenResponseClient(
new DefaultAuthorizationCodeTokenResponseClient(), tokenDecoderFactory);
}
return accessTokenResponseClient;
}
/** @param pAccessTokenResponseClient the accessTokenResponseClient to set */
public void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> pAccessTokenResponseClient) {
accessTokenResponseClient = pAccessTokenResponseClient;
}
/** @return the httpSecurityCustomizer */
public Consumer<HttpSecurity> getHttpSecurityCustomizer() {
return httpSecurityCustomizer;
}
/** @param pConfiguration the configuration to set */
public void setConfiguration(GeoServerOAuth2LoginFilterConfig pConfiguration) {
configuration = pConfiguration;
}
/** @param pHttp the http to set */
public void setHttp(HttpSecurity pHttp) {
http = pHttp;
}
/** @param pTokenDecoderFactory the tokenDecoderFactory to set */
public void setTokenDecoderFactory(GeoServerOidcIdTokenDecoderFactory pTokenDecoderFactory) {
tokenDecoderFactory = pTokenDecoderFactory;
}
/** @return the tokenDecoderFactory */
public GeoServerOidcIdTokenDecoderFactory getTokenDecoderFactory() {
return tokenDecoderFactory;
}
/** @param pSecurityManager the securityManager to set */
public void setSecurityManager(GeoServerSecurityManager pSecurityManager) {
securityManager = pSecurityManager;
}
/** @param pEventPublisher the eventPublisher to set */
public void setEventPublisher(ApplicationEventPublisher pEventPublisher) {
eventPublisher = pEventPublisher;
}
/** @param pHttpSecurityCustomizer the httpSecurityCustomizer to set */
public void setHttpSecurityCustomizer(HttpSecurityCustomizer pHttpSecurityCustomizer) {
if (pHttpSecurityCustomizer != null) {
httpSecurityCustomizer = pHttpSecurityCustomizer;
}
}
/** @param pClientRegistrationCustomizer the clientRegistrationCustomizer to set */
public void setClientRegistrationCustomizer(ClientRegistrationCustomizer pClientRegistrationCustomizer) {
if (pClientRegistrationCustomizer != null) {
clientRegistrationCustomizer = pClientRegistrationCustomizer;
}
}
}

View File

@ -0,0 +1,128 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import java.util.logging.Logger;
import org.geoserver.config.GeoServer;
import org.geoserver.config.util.XStreamPersister;
import org.geoserver.logging.LoggingUtils;
import org.geoserver.platform.ContextLoadedEvent;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.config.SecurityNamedServiceConfig;
import org.geoserver.security.filter.AbstractFilterProvider;
import org.geoserver.security.filter.GeoServerSecurityFilter;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginCustomizers.ClientRegistrationCustomizer;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginCustomizers.FilterBuilderCustomizer;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginCustomizers.HttpSecurityCustomizer;
import org.geoserver.security.validation.SecurityConfigValidator;
import org.geotools.util.logging.Logging;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
/**
* Creates a {@link GeoServerOAuth2LoginAuthenticationFilter} which supports OAuth2 and OpenID Connect login by
* delegating to Spring's respective filters.
*
* <p>This provider uses the Spring public API ({@link HttpSecurity}) to setup the required Spring filters. Advantage:
* It's a hopefully future proof way of settings up the filters and their related objects. Disadvantage: Spring API is
* not designed to support changing of configuration in running applications. Effect: When saving changes via the admin
* UI, some then obsolete instances remain in the Spring factories as "disposableBeans". However, the memory footprint
* of these classes is small and no negative impact on the application has been identified, as the objects are no longer
* used. The advantages of this approach seem to outweigh the disadvantages.
*
* @author awaterme
* @see https://github.com/spring-projects/spring-security/issues/7449 (currently in status "open") regarding more
* flexible configuration
*/
public class GeoServerOAuth2LoginAuthenticationProvider extends AbstractFilterProvider
implements ApplicationListener<ApplicationEvent>, GeoServerOAuth2ClientRegistrationId {
private static final Logger LOGGER = Logging.getLogger(GeoServerOAuth2LoginAuthenticationProvider.class);
private GeoServerSecurityManager securityManager;
private ApplicationContext context;
private String builderBeanName = "geoServerOAuth2LoginAuthenticationFilterBuilder";
public GeoServerOAuth2LoginAuthenticationProvider(GeoServerSecurityManager pSecurityManager) {
this.securityManager = pSecurityManager;
context = pSecurityManager.getApplicationContext();
}
@Override
public void configure(XStreamPersister xp) {
xp.getXStream().alias("oauth2LoginAuthentication", GeoServerOAuth2LoginFilterConfig.class);
}
@Override
public Class<? extends GeoServerSecurityFilter> getFilterClass() {
return GeoServerOAuth2LoginAuthenticationFilter.class;
}
@Override
public GeoServerSecurityFilter createFilter(SecurityNamedServiceConfig config) {
GeoServerOAuth2LoginFilterConfig lConfig = (GeoServerOAuth2LoginFilterConfig) config;
LOGGER.fine("Using '" + builderBeanName + "' for filter creation");
HttpSecurityCustomizer lHttpCustomizer;
ClientRegistrationCustomizer lClientCustomizer;
FilterBuilderCustomizer lBuilderCustomizer;
GeoServerOAuth2LoginAuthenticationFilterBuilder lBuilder;
lHttpCustomizer = getOptionalBean(HttpSecurityCustomizer.class);
lClientCustomizer = getOptionalBean(ClientRegistrationCustomizer.class);
lBuilderCustomizer = getOptionalBean(FilterBuilderCustomizer.class);
lBuilder = context.getBean(builderBeanName, GeoServerOAuth2LoginAuthenticationFilterBuilder.class);
lBuilder.setConfiguration(lConfig);
lBuilder.setSecurityManager(securityManager);
lBuilder.setEventPublisher(context);
lBuilder.setHttpSecurityCustomizer(lHttpCustomizer);
lBuilder.setClientRegistrationCustomizer(lClientCustomizer);
if (lBuilderCustomizer != null) {
lBuilderCustomizer.accept(lBuilder);
}
GeoServerOAuth2LoginAuthenticationFilter lFilter = lBuilder.build();
return lFilter;
}
private <T> T getOptionalBean(Class<T> pClass) {
try {
return context.getBean(pClass);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
@Override
public SecurityConfigValidator createConfigurationValidator(GeoServerSecurityManager securityManager) {
return new GeoServerOAuth2LoginFilterConfigValidator(securityManager);
}
/**
* Provide a helpful OIDC_LOGGING configuration for this extension on context load event.
*
* @param event application event, responds ContextLoadEvent
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextLoadedEvent) {
// provide a helpful logging config for this extension
GeoServer geoserver = GeoServerExtensions.bean(GeoServer.class, this.context);
GeoServerResourceLoader loader = geoserver.getCatalog().getResourceLoader();
LoggingUtils.checkBuiltInLoggingConfiguration(loader, "OIDC_LOGGING");
}
}
/** @param pBuildBeanName the buildBeanName to set */
public void setBuilderBeanName(String pBuildBeanName) {
builderBeanName = pBuildBeanName;
}
}

View File

@ -0,0 +1,23 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import java.util.function.Consumer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* Defines interfaces for optional customizers, which might be used to tweak the OIDC login in a specific way.
*
* @author awaterme
*/
public class GeoServerOAuth2LoginCustomizers {
public interface HttpSecurityCustomizer extends Consumer<HttpSecurity> {}
public interface ClientRegistrationCustomizer extends Consumer<ClientRegistration> {}
public interface FilterBuilderCustomizer extends Consumer<GeoServerOAuth2LoginAuthenticationFilterBuilder> {}
}

View File

@ -0,0 +1,574 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import static java.util.Optional.ofNullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.geoserver.config.GeoServer;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.config.SecurityAuthFilterConfig;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
/**
* Filter configuration for OAuth2 and OpenID Connect.
*
* @author awaterme
*/
public class GeoServerOAuth2LoginFilterConfig extends PreAuthenticatedUserNameFilterConfig
implements SecurityAuthFilterConfig, GeoServerOAuth2ClientRegistrationId {
private static final long serialVersionUID = -8581346584859849804L;
/** Supports extraction of roles among the token claims */
public static enum OpenIdRoleSource implements RoleSource {
IdToken,
AccessToken,
MSGraphAPI,
UserInfo;
@Override
public boolean equals(RoleSource other) {
return other != null && other.toString().equals(toString());
}
}
/**
* Constant used to setup the proxy base in tests that are running without a GeoServer instance or an actual HTTP
* request context. The value of the variable is set-up in the pom.xml, as a system property for surefire, in order
* to avoid hard-coding the value in the code.
*/
public static final String OPENID_TEST_GS_PROXY_BASE = "OPENID_TEST_GS_PROXY_BASE";
// Common for all providers
private String baseRedirectUri = baseRedirectUri();
// Google
private boolean googleEnabled;
private String googleClientId;
private String googleClientSecret;
private String googleUserNameAttribute = "email";
private String googleRedirectUri;
// GitHub
private boolean gitHubEnabled;
private String gitHubClientId;
private String gitHubClientSecret;
private String gitHubUserNameAttribute = "id";
private String gitHubRedirectUri;
// Microsoft Azure
private boolean msEnabled;
private String msClientId;
private String msClientSecret;
private String msUserNameAttribute = "sub";
private String msRedirectUri;
private String msScopes = "openid profile email";
// custom OpenID Connect
private boolean oidcEnabled;
private String oidcClientId;
private String oidcClientSecret;
private String oidcUserNameAttribute = "email";
private String oidcRedirectUri;
private String oidcScopes = "openid";
private String oidcDiscoveryUri;
private String oidcTokenUri;
private String oidcAuthorizationUri;
private String oidcUserInfoUri;
private String oidcJwkSetUri;
private String oidcLogoutUri;
private String oidcResponseMode;
/** currently no UI counterpart */
private String oidcJwsAlgorithmName;
private boolean oidcForceAuthorizationUriHttps = true;
private boolean oidcForceTokenUriHttps = true;
private boolean oidcEnforceTokenValidation = true;
private boolean oidcUsePKCE = false;
private boolean oidcAuthenticationMethodPostSecret = false;
/**
* Add extra logging. NOTE: this might spill confidential information to the log - do not turn on in normal
* operation!
*/
private boolean oidcAllowUnSecureLogging = false;
// further common attributes affecting all providers
private String tokenRolesClaim;
private String postLogoutRedirectUri;
private boolean enableRedirectAuthenticationEntryPoint;
public GeoServerOAuth2LoginFilterConfig() {
this.postLogoutRedirectUri = createPostLogoutRedirectUri();
this.calculateRedirectUris();
}
public void calculateRedirectUris() {
this.googleRedirectUri = redirectUri(REG_ID_GOOGLE);
this.gitHubRedirectUri = redirectUri(REG_ID_GIT_HUB);
this.msRedirectUri = redirectUri(REG_ID_MICROSOFT);
this.oidcRedirectUri = redirectUri(REG_ID_OIDC);
}
private String redirectUri(String pRegId) {
String lBase = baseRedirectUriNormalized();
return lBase + "login/oauth2/code/" + pRegId;
}
private String createPostLogoutRedirectUri() {
String lBase = baseRedirectUri();
if (!lBase.endsWith("/web/")) {
lBase += "web/";
}
return lBase;
}
/** @return an URI ending with "/" */
private String baseRedirectUriNormalized() {
return ofNullable(baseRedirectUri)
.map(s -> s.endsWith("/") ? s : s + "/")
.orElse("/");
}
public String getAuthenticationEntryPointRedirectUri() {
List<String> lRegIds = new ArrayList<>();
if (isGoogleEnabled()) {
lRegIds.add(REG_ID_GOOGLE);
}
if (isGitHubEnabled()) {
lRegIds.add(REG_ID_GIT_HUB);
}
if (isMsEnabled()) {
lRegIds.add(REG_ID_MICROSOFT);
}
if (isOidcEnabled()) {
lRegIds.add(REG_ID_OIDC);
}
if (lRegIds.isEmpty() || 1 < lRegIds.size()) {
return null;
}
String lBase = baseRedirectUriNormalized();
return lBase + "oauth2/authorization/" + lRegIds.get(0);
}
/**
* we add "/" at the end since not having it will SOMETIME cause issues. This will either use the proxyBaseURL (if
* set), or from ServletUriComponentsBuilder.fromCurrentContextPath().
*
* @return
*/
String baseRedirectUri() {
Optional<String> proxbaseUrl = Optional.ofNullable(GeoServerExtensions.bean(GeoServer.class))
.map(gs -> gs.getSettings())
.map(s -> s.getProxyBaseUrl());
if (proxbaseUrl.isPresent() && StringUtils.hasText(proxbaseUrl.get())) {
return proxbaseUrl + "/";
}
if (RequestContextHolder.getRequestAttributes() != null)
return ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString() + "/";
// fallback to run tests without a full environment
return GeoServerExtensions.getProperty(OPENID_TEST_GS_PROXY_BASE);
}
public String getOidcUserNameAttribute() {
return oidcUserNameAttribute;
}
@Override
public boolean providesAuthenticationEntryPoint() {
return false;
}
public int getActiveProviderCount() {
int lActiveCount = isGoogleEnabled() ? 1 : 0;
lActiveCount += isGitHubEnabled() ? 1 : 0;
lActiveCount += isMsEnabled() ? 1 : 0;
lActiveCount += isOidcEnabled() ? 1 : 0;
return lActiveCount;
}
/** @return the cliendId */
public String getOidcClientId() {
return oidcClientId;
}
/** @param cliendId the cliendId to set */
public void setOidcClientId(String cliendId) {
this.oidcClientId = cliendId;
}
/** @return the clientSecret */
public String getOidcClientSecret() {
return oidcClientSecret;
}
/** @param clientSecret the clientSecret to set */
public void setOidcClientSecret(String clientSecret) {
this.oidcClientSecret = clientSecret;
}
/** @return the accessTokenUri */
public String getOidcTokenUri() {
return oidcTokenUri;
}
/** @param accessTokenUri the accessTokenUri to set */
public void setOidcTokenUri(String accessTokenUri) {
this.oidcTokenUri = accessTokenUri;
}
/** @return the userAuthorizationUri */
public String getOidcAuthorizationUri() {
return oidcAuthorizationUri;
}
/** @param userAuthorizationUri the userAuthorizationUri to set */
public void setOidcAuthorizationUri(String userAuthorizationUri) {
this.oidcAuthorizationUri = userAuthorizationUri;
}
/** @return the redirectUri */
public String getOidcRedirectUri() {
return oidcRedirectUri;
}
/** @param redirectUri the redirectUri to set */
public void setOidcRedirectUri(String redirectUri) {
this.oidcRedirectUri = redirectUri;
}
/** @return the checkTokenEndpointUrl */
public String getOidcUserInfoUri() {
return oidcUserInfoUri;
}
/** @param checkTokenEndpointUrl the checkTokenEndpointUrl to set */
public void setOidcUserInfoUri(String checkTokenEndpointUrl) {
this.oidcUserInfoUri = checkTokenEndpointUrl;
}
/** @return the logoutUri */
public String getOidcLogoutUri() {
return oidcLogoutUri;
}
/** @param logoutUri the logoutUri to set */
public void setOidcLogoutUri(String logoutUri) {
this.oidcLogoutUri = logoutUri;
}
/** @return the scopes */
public String getOidcScopes() {
return oidcScopes;
}
/** @param scopes the scopes to set */
public void setOidcScopes(String scopes) {
this.oidcScopes = scopes;
}
/** @return the enableRedirectAuthenticationEntryPoint */
public boolean getEnableRedirectAuthenticationEntryPoint() {
return enableRedirectAuthenticationEntryPoint;
}
/** @param enableRedirectAuthenticationEntryPoint the enableRedirectAuthenticationEntryPoint to set */
public void setEnableRedirectAuthenticationEntryPoint(boolean enableRedirectAuthenticationEntryPoint) {
this.enableRedirectAuthenticationEntryPoint = enableRedirectAuthenticationEntryPoint;
}
public boolean getOidcForceTokenUriHttps() {
return oidcForceTokenUriHttps;
}
public void setOidcForceTokenUriHttps(boolean forceAccessTokenUriHttps) {
this.oidcForceTokenUriHttps = forceAccessTokenUriHttps;
}
public boolean getOidcForceAuthorizationUriHttps() {
return oidcForceAuthorizationUriHttps;
}
public void setOidcForceAuthorizationUriHttps(boolean forceUserAuthorizationUriHttps) {
this.oidcForceAuthorizationUriHttps = forceUserAuthorizationUriHttps;
}
public void setOidcUserNameAttribute(String principalKey) {
this.oidcUserNameAttribute = principalKey;
}
public String getOidcJwkSetUri() {
return oidcJwkSetUri;
}
public void setOidcJwkSetUri(String jwkURI) {
this.oidcJwkSetUri = jwkURI;
}
public String getTokenRolesClaim() {
return tokenRolesClaim;
}
public void setTokenRolesClaim(String tokenRolesClaim) {
this.tokenRolesClaim = tokenRolesClaim;
}
public String getOidcResponseMode() {
return oidcResponseMode;
}
public void setOidcResponseMode(String responseMode) {
this.oidcResponseMode = responseMode;
}
public boolean isOidcAuthenticationMethodPostSecret() {
return oidcAuthenticationMethodPostSecret;
}
public void setOidcAuthenticationMethodPostSecret(boolean sendClientSecret) {
this.oidcAuthenticationMethodPostSecret = sendClientSecret;
}
public String getPostLogoutRedirectUri() {
return postLogoutRedirectUri;
}
public void setPostLogoutRedirectUri(String postLogoutRedirectUri) {
this.postLogoutRedirectUri = postLogoutRedirectUri;
}
public boolean isOidcUsePKCE() {
return oidcUsePKCE;
}
public void setOidcUsePKCE(boolean usePKCE) {
this.oidcUsePKCE = usePKCE;
}
public boolean isOidcEnforceTokenValidation() {
return oidcEnforceTokenValidation;
}
public void setOidcEnforceTokenValidation(boolean enforceTokenValidation) {
this.oidcEnforceTokenValidation = enforceTokenValidation;
}
/** @return the googleEnabled */
public boolean isGoogleEnabled() {
return googleEnabled;
}
/** @param pGoogleEnabled the googleEnabled to set */
public void setGoogleEnabled(boolean pGoogleEnabled) {
googleEnabled = pGoogleEnabled;
}
/** @return the googleCliendId */
public String getGoogleClientId() {
return googleClientId;
}
/** @param pGoogleCliendId the googleCliendId to set */
public void setGoogleClientId(String pGoogleCliendId) {
googleClientId = pGoogleCliendId;
}
/** @return the googleClientSecret */
public String getGoogleClientSecret() {
return googleClientSecret;
}
/** @param pGoogleClientSecret the googleClientSecret to set */
public void setGoogleClientSecret(String pGoogleClientSecret) {
googleClientSecret = pGoogleClientSecret;
}
/** @return the googleUserNameAttribute */
public String getGoogleUserNameAttribute() {
return googleUserNameAttribute;
}
/** @param pGoogleUserNameAttribute the googleUserNameAttribute to set */
public void setGoogleUserNameAttribute(String pGoogleUserNameAttribute) {
googleUserNameAttribute = pGoogleUserNameAttribute;
}
/** @return the gitHubEnabled */
public boolean isGitHubEnabled() {
return gitHubEnabled;
}
/** @param pGitHubEnabled the gitHubEnabled to set */
public void setGitHubEnabled(boolean pGitHubEnabled) {
gitHubEnabled = pGitHubEnabled;
}
/** @return the gitHubClientId */
public String getGitHubClientId() {
return gitHubClientId;
}
/** @param pGitHubClientId the gitHubClientId to set */
public void setGitHubClientId(String pGitHubClientId) {
gitHubClientId = pGitHubClientId;
}
/** @return the gitHubClientSecret */
public String getGitHubClientSecret() {
return gitHubClientSecret;
}
/** @param pGitHubClientSecret the gitHubClientSecret to set */
public void setGitHubClientSecret(String pGitHubClientSecret) {
gitHubClientSecret = pGitHubClientSecret;
}
/** @return the gitHubUserNameAttribute */
public String getGitHubUserNameAttribute() {
return gitHubUserNameAttribute;
}
/** @param pGitHubUserNameAttribute the gitHubUserNameAttribute to set */
public void setGitHubUserNameAttribute(String pGitHubUserNameAttribute) {
gitHubUserNameAttribute = pGitHubUserNameAttribute;
}
/** @return the enabled */
public boolean isOidcEnabled() {
return oidcEnabled;
}
/** @param pEnabled the enabled to set */
public void setOidcEnabled(boolean pEnabled) {
oidcEnabled = pEnabled;
}
/** @return the msEnabled */
public boolean isMsEnabled() {
return msEnabled;
}
/** @param pMsEnabled the msEnabled to set */
public void setMsEnabled(boolean pMsEnabled) {
msEnabled = pMsEnabled;
}
/** @return the msClientId */
public String getMsClientId() {
return msClientId;
}
/** @param pMsClientId the msClientId to set */
public void setMsClientId(String pMsClientId) {
msClientId = pMsClientId;
}
/** @return the msClientSecret */
public String getMsClientSecret() {
return msClientSecret;
}
/** @param pMsClientSecret the msClientSecret to set */
public void setMsClientSecret(String pMsClientSecret) {
msClientSecret = pMsClientSecret;
}
/** @return the msNameAttribute */
public String getMsUserNameAttribute() {
return msUserNameAttribute;
}
/** @param pMsNameAttribute the msNameAttribute to set */
public void setMsUserNameAttribute(String pMsNameAttribute) {
msUserNameAttribute = pMsNameAttribute;
}
/** @return the baseRedirectUri */
public String getBaseRedirectUri() {
return baseRedirectUri;
}
/** @param pBaseRedirectUri the baseRedirectUri to set */
public void setBaseRedirectUri(String pBaseRedirectUri) {
baseRedirectUri = pBaseRedirectUri;
}
/** @return the googleRedirectUri */
public String getGoogleRedirectUri() {
return googleRedirectUri;
}
/** @param pGoogleRedirectUri the googleRedirectUri to set */
public void setGoogleRedirectUri(String pGoogleRedirectUri) {
googleRedirectUri = pGoogleRedirectUri;
}
/** @return the gitHubRedirectUri */
public String getGitHubRedirectUri() {
return gitHubRedirectUri;
}
/** @param pGitHubRedirectUri the gitHubRedirectUri to set */
public void setGitHubRedirectUri(String pGitHubRedirectUri) {
gitHubRedirectUri = pGitHubRedirectUri;
}
/** @return the msRedirectUri */
public String getMsRedirectUri() {
return msRedirectUri;
}
/** @param pMsRedirectUri the msRedirectUri to set */
public void setMsRedirectUri(String pMsRedirectUri) {
msRedirectUri = pMsRedirectUri;
}
/** @return the msScopes */
public String getMsScopes() {
return msScopes;
}
/** @param pMsScopes the msScopes to set */
public void setMsScopes(String pMsScopes) {
msScopes = pMsScopes;
}
/** @return the oidcDiscoveryURL */
public String getOidcDiscoveryUri() {
return oidcDiscoveryUri;
}
/** @param pOidcDiscoveryURL the oidcDiscoveryURL to set */
public void setOidcDiscoveryUri(String pOidcDiscoveryURL) {
oidcDiscoveryUri = pOidcDiscoveryURL;
}
/** @return the allowUnSecureLogging */
public boolean isOidcAllowUnSecureLogging() {
return oidcAllowUnSecureLogging;
}
/** @param pAllowUnSecureLogging the allowUnSecureLogging to set */
public void setOidcAllowUnSecureLogging(boolean pAllowUnSecureLogging) {
oidcAllowUnSecureLogging = pAllowUnSecureLogging;
}
/** @return the jwsAlgorithmName */
public String getOidcJwsAlgorithmName() {
return oidcJwsAlgorithmName;
}
/** @param pJwsAlgorithmName the jwsAlgorithmName to set */
public void setOidcJwsAlgorithmName(String pJwsAlgorithmName) {
oidcJwsAlgorithmName = pJwsAlgorithmName;
}
}

View File

@ -0,0 +1,303 @@
/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.geoserver.platform.exception.GeoServerRuntimException;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.config.SecurityNamedServiceConfig;
import org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource;
import org.geoserver.security.validation.FilterConfigException;
import org.geoserver.security.validation.FilterConfigValidator;
import org.springframework.util.StringUtils;
/**
* Validates {@link GeoServerOAuth2LoginFilterConfig} objects.
*
* @author Alessio Fabiani, GeoSolutions S.A.S.
* @author awaterme
*/
public class GeoServerOAuth2LoginFilterConfigValidator extends FilterConfigValidator {
public GeoServerOAuth2LoginFilterConfigValidator(GeoServerSecurityManager securityManager) {
super(securityManager);
}
@Override
public void validateFilterConfig(SecurityNamedServiceConfig config) throws FilterConfigException {
if (config instanceof GeoServerOAuth2LoginFilterConfig) {
validateOAuth2FilterConfig((GeoServerOAuth2LoginFilterConfig) config);
} else {
super.validateFilterConfig(config);
}
}
public void validateOAuth2FilterConfig(GeoServerOAuth2LoginFilterConfig filterConfig) throws FilterConfigException {
super.validateFilterConfig((SecurityNamedServiceConfig) filterConfig);
validNoOtherInstance(filterConfig);
String lProviderName = "OpenID Connect";
if (filterConfig.isOidcEnabled()) {
validateUserNameAttribute(filterConfig.getOidcUserNameAttribute(), lProviderName);
validateOidcAuthorizationUri(filterConfig);
validateOidcTokenUri(filterConfig);
validateOidcLogoutUri(filterConfig);
validateOidcUserInfoUri(filterConfig);
validateOidcRedirectUri(filterConfig);
validateClientId(filterConfig.getOidcClientId(), lProviderName);
if (!filterConfig.isOidcUsePKCE()) {
validateClientSecret(filterConfig.getOidcClientSecret(), lProviderName);
}
validateScopes(filterConfig.getOidcScopes(), lProviderName);
validateOidcJwkSet(filterConfig);
validateAuthenticationEntryPoint(filterConfig);
}
lProviderName = "Google";
if (filterConfig.isGoogleEnabled()) {
validateUserNameAttribute(filterConfig.getGoogleUserNameAttribute(), lProviderName);
validateClientId(filterConfig.getGoogleClientId(), lProviderName);
validateClientSecret(filterConfig.getGoogleClientSecret(), lProviderName);
}
lProviderName = "GitHub";
if (filterConfig.isGitHubEnabled()) {
validateUserNameAttribute(filterConfig.getGitHubUserNameAttribute(), lProviderName);
validateClientId(filterConfig.getGitHubClientId(), lProviderName);
validateClientSecret(filterConfig.getGitHubClientSecret(), lProviderName);
}
lProviderName = "Microsoft Azure";
if (filterConfig.isMsEnabled()) {
validateUserNameAttribute(filterConfig.getMsUserNameAttribute(), lProviderName);
validateClientId(filterConfig.getMsClientId(), lProviderName);
validateClientSecret(filterConfig.getMsClientSecret(), lProviderName);
validateScopes(filterConfig.getMsScopes(), lProviderName);
}
validateRoleSourceMsGraph(filterConfig);
validateRoleSourceIdToken(filterConfig);
validateRoleSourceUserInfo(filterConfig);
}
private void validateRoleSourceUserInfo(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
if (OpenIdRoleSource.UserInfo.equals(filterConfig.getRoleSource())
&& !StringUtils.hasLength(filterConfig.getOidcUserInfoUri())) {
throw createFilterException(GeoServerOAuth2FilterConfigException.ROLE_SOURCE_USER_INFO_URI_REQUIRED);
}
}
private void validateRoleSourceIdToken(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
if (OpenIdRoleSource.IdToken.equals(filterConfig.getRoleSource()) && filterConfig.isGitHubEnabled()) {
throw createFilterException(GeoServerOAuth2FilterConfigException.ROLE_SOURCE_ID_TOKEN_INVALID_FOR_GITHUB);
}
}
/**
* @param pFilterConfig
* @throws GeoServerOAuth2FilterConfigException
*/
private void validateRoleSourceMsGraph(GeoServerOAuth2LoginFilterConfig pFilterConfig)
throws GeoServerOAuth2FilterConfigException {
RoleSource lRoleSource = pFilterConfig.getRoleSource();
if (!OpenIdRoleSource.MSGraphAPI.equals(lRoleSource)) {
return;
}
int lCount = pFilterConfig.getActiveProviderCount();
boolean lNoEnabled = lCount == 0;
boolean lOnlyMs = pFilterConfig.isMsEnabled() && lCount == 1;
if (!(lNoEnabled || lOnlyMs)) {
throw createFilterException(GeoServerOAuth2FilterConfigException.MSGRAPH_COMBINATION_INVALID);
}
}
/**
* @param pFilterConfig
* @throws GeoServerOAuth2FilterConfigException
*/
private void validateAuthenticationEntryPoint(GeoServerOAuth2LoginFilterConfig pFilterConfig)
throws GeoServerOAuth2FilterConfigException {
if (pFilterConfig.getEnableRedirectAuthenticationEntryPoint()) {
int lActiveCount = pFilterConfig.getActiveProviderCount();
if (lActiveCount != 1) {
throw createFilterException(GeoServerOAuth2FilterConfigException.AEP_DENIED_WRONG_PROVIDER_COUNT);
}
}
}
private void validateOidcJwkSet(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
// currently required
// when UI option to choose algorithm is introduced, this becomes conditionally optional
if (!StringUtils.hasLength(filterConfig.getOidcJwkSetUri())) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_JWK_SET_URI_REQUIRED);
}
try {
new URL(filterConfig.getOidcJwkSetUri());
} catch (MalformedURLException ex) {
throw new GeoServerOAuth2FilterConfigException(
GeoServerOAuth2FilterConfigException.OAUTH2_WKTS_URL_MALFORMED);
}
}
private void validateScopes(String pScopes, String pProviderName) throws GeoServerOAuth2FilterConfigException {
if (!StringUtils.hasLength(pScopes)) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_SCOPE_REQUIRED, pProviderName);
}
String[] lScopes = ScopeUtils.valueOf(pScopes);
boolean lMix = Arrays.stream(lScopes).anyMatch(s -> s.contains(" "));
if (lMix) {
throw createFilterException(
GeoServerOAuth2FilterConfigException.OAUTH2_SCOPE_DELIMITER_MIXED, pProviderName);
}
}
private void validateClientId(String pClientId, String pProviderName) throws GeoServerOAuth2FilterConfigException {
if (!StringUtils.hasLength(pClientId)) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_ID_REQUIRED, pProviderName);
}
}
private void validateUserNameAttribute(String pUserName, String pProviderName)
throws GeoServerOAuth2FilterConfigException {
if (!StringUtils.hasLength(pUserName)) {
throw createFilterException(
GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_USER_NAME_REQUIRED, pProviderName);
}
}
private void validateOidcRedirectUri(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
if (StringUtils.hasLength(filterConfig.getOidcRedirectUri())) {
try {
new URL(filterConfig.getOidcRedirectUri());
} catch (MalformedURLException ex) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_REDIRECT_URI_MALFORMED);
}
}
}
private void validateOidcLogoutUri(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
if (StringUtils.hasLength(filterConfig.getOidcLogoutUri())) {
try {
new URL(filterConfig.getOidcLogoutUri());
} catch (MalformedURLException ex) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_URL_IN_LOGOUT_URI_MALFORMED);
}
}
}
private void validateOidcAuthorizationUri(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
if (!StringUtils.hasLength(filterConfig.getOidcAuthorizationUri())) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_USERAUTHURI_MALFORMED);
}
if (StringUtils.hasLength(filterConfig.getOidcAuthorizationUri())) {
URL userAuthorizationUri = null;
try {
userAuthorizationUri = new URL(filterConfig.getOidcAuthorizationUri());
} catch (MalformedURLException ex) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_USERAUTHURI_MALFORMED);
}
if (filterConfig.getOidcForceAuthorizationUriHttps()
&& "https".equalsIgnoreCase(userAuthorizationUri.getProtocol()) == false)
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_USERAUTHURI_NOT_HTTPS);
}
}
private void validateOidcTokenUri(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
if (!StringUtils.hasLength(filterConfig.getOidcTokenUri())) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_ACCESSTOKENURI_MALFORMED);
}
if (StringUtils.hasLength(filterConfig.getOidcTokenUri())) {
URL accessTokenUri = null;
try {
accessTokenUri = new URL(filterConfig.getOidcTokenUri());
} catch (MalformedURLException ex) {
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_ACCESSTOKENURI_MALFORMED);
}
if (filterConfig.getOidcForceTokenUriHttps()
&& "https".equalsIgnoreCase(accessTokenUri.getProtocol()) == false)
throw createFilterException(GeoServerOAuth2FilterConfigException.OAUTH2_ACCESSTOKENURI_NOT_HTTPS);
}
}
private void validNoOtherInstance(GeoServerOAuth2LoginFilterConfig filterConfig)
throws GeoServerOAuth2FilterConfigException {
Set<String> lOAuthFilterNames;
try {
lOAuthFilterNames = manager.listFilters(GeoServerOAuth2LoginAuthenticationFilter.class);
} catch (IOException e) {
throw new GeoServerRuntimException("Validation failed. Error while listing existing filters.", e);
}
if (lOAuthFilterNames == null) {
lOAuthFilterNames = new HashSet<>();
}
lOAuthFilterNames.remove(filterConfig.getName());
if (!lOAuthFilterNames.isEmpty()) {
throw createFilterException(
"OAUTH2_MULTIPLE_INSTANCE_NOT_SUPPORTED",
lOAuthFilterNames.iterator().next());
}
}
/** Only require checkTokenEndpointUrl if JSON Web Key set URI is empty. */
private void validateOidcUserInfoUri(GeoServerOAuth2LoginFilterConfig filterConfig) throws FilterConfigException {
// Note: Spring uses userInfoEndpoint OIDC case if a) specified and b) scopes require it, see
// OidcUserService.shouldRetrieveUserInfo
boolean lUriPresent = StringUtils.hasLength(filterConfig.getOidcUserInfoUri());
List<String> lScopes = Arrays.asList(ScopeUtils.valueOf(filterConfig.getOidcScopes()));
boolean lOidcScopePresent = lScopes.contains("openid");
if (!lUriPresent) {
if (!lOidcScopePresent) {
throw new GeoServerOAuth2FilterConfigException(
GeoServerOAuth2FilterConfigException.OAUTH2_USER_INFO_URI_REQUIRED_NO_OIDC);
}
} else {
try {
new URL(filterConfig.getOidcUserInfoUri());
} catch (MalformedURLException ex) {
throw createFilterException(
GeoServerOAuth2FilterConfigException.OAUTH2_CHECKTOKENENDPOINT_URL_MALFORMED);
}
}
}
/**
* Validate {@code client_secret} if required.
*
* <p>Default implementation requires {@code client_secret} to be provided. Subclasses can override if working with
* a public client that cannot keep a secret.
*
* @param pClientSecret
* @param pProviderName
*/
private void validateClientSecret(String pClientSecret, String pProviderName) throws FilterConfigException {
if (!StringUtils.hasLength(pClientSecret)) {
throw createFilterException(
GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_SECRET_REQUIRED, pProviderName);
}
}
@Override
protected GeoServerOAuth2FilterConfigException createFilterException(String errorid, Object... args) {
return new GeoServerOAuth2FilterConfigException(errorid, args);
}
}

View File

@ -0,0 +1,51 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import org.springframework.context.ApplicationEvent;
/**
* Event signals a changed OAuth2/OIDC provider activation. Fired on configuration changes, triggers visibility updates
* for login buttons.
*
* @author awaterme
*/
public class OAuth2LoginButtonEnablementEvent extends ApplicationEvent {
/** serialVersionUID */
private static final long serialVersionUID = 513879448251262654L;
public static final OAuth2LoginButtonEnablementEvent enableButtonEvent(Object pSource, String pId) {
return new OAuth2LoginButtonEnablementEvent(pSource, true, pId);
}
public static final OAuth2LoginButtonEnablementEvent disableButtonEvent(Object pSource, String pId) {
return new OAuth2LoginButtonEnablementEvent(pSource, false, pId);
}
private boolean enable;
private String registrationId;
/**
* @param pSource
* @param pEnable
* @param pRegistrationId
*/
public OAuth2LoginButtonEnablementEvent(Object pSource, boolean pEnable, String pRegistrationId) {
super(pSource);
enable = pEnable;
registrationId = pRegistrationId;
}
/** @return the enable */
public boolean isEnable() {
return enable;
}
/** @return the registrationId */
public String getRegistrationId() {
return registrationId;
}
}

View File

@ -0,0 +1,34 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
/**
* Provides scope related utilities.
*
* @author awaterme
*/
public class ScopeUtils {
/**
* Turns a scope text, separated by comma or space into scopes
*
* @param pScopeList
* @return An array, maybe empty
*/
public static String[] valueOf(String pScopeList) {
if (pScopeList == null || pScopeList.isBlank()) {
return new String[] {};
}
if (pScopeList.contains(",")) {
String[] lScopes = pScopeList.trim().split("\\s*,\\s*");
return lScopes;
}
if (pScopeList.contains(" ")) {
String[] lScopes = pScopeList.trim().split("\\s+");
return lScopes;
}
return new String[] {pScopeList.trim()};
}
}

View File

@ -0,0 +1,54 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.resourceserver;
import java.util.Collection;
import org.geoserver.security.filter.GeoServerRoleResolvers;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverParam;
import org.geoserver.security.impl.GeoServerRole;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
/**
* {@link Jwt} converter considering GeoServer basic role sources for authorization.
*
* <p>Used for the "Resource Server" use case. Implementation is unfinished, because a different GS extension supports
* this case already. Filter is not offered in UI. This code is never executed.
*
* @author awaterme
*/
public class GeoServerJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private JwtAuthenticationConverter delegate = new JwtAuthenticationConverter();
private GeoServerRoleResolvers.ResolverContext roleResolverContext;
public GeoServerJwtAuthenticationConverter(GeoServerRoleResolvers.ResolverContext pCtx) {
super();
roleResolverContext = pCtx;
}
@Override
public AbstractAuthenticationToken convert(Jwt pSource) {
JwtAuthenticationToken lToken = (JwtAuthenticationToken) delegate.convert(pSource);
if (lToken == null) {
return null;
}
String lPrincipal = lToken.getName();
GeoServerRoleResolvers.RoleResolver lResolver = GeoServerRoleResolvers.PRE_AUTH_ROLE_SOURCE_RESOLVER;
Collection<GeoServerRole> lRoles = lResolver.convert(new ResolverParam(lPrincipal, null, roleResolverContext));
JwtAuthenticationToken lNew = new JwtAuthenticationToken(pSource, lRoles, lPrincipal);
return lNew;
}
public void setPrincipalClaimName(String pName) {
delegate.setPrincipalClaimName(pName);
}
}

View File

@ -0,0 +1,59 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.resourceserver;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.geoserver.security.config.SecurityNamedServiceConfig;
import org.geoserver.security.filter.GeoServerAuthenticationFilter;
import org.geoserver.security.filter.GeoServerCompositeFilter;
import org.geotools.util.logging.Logging;
/**
* {@link Filter} supports OAuth2 resource server scenarios by delegating to the nested Spring filter implementations.
*
* <p>Used for the "Resource Server" use case. Implementation is unfinished, because a different GS extension supports
* this case already. Filter is not offered in UI. This code is never executed.
*
* @author awaterme
*/
public class GeoServerOAuth2ResourceServerAuthenticationFilter extends GeoServerCompositeFilter
implements GeoServerAuthenticationFilter {
private static final Logger LOGGER = Logging.getLogger(GeoServerOAuth2ResourceServerAuthenticationFilter.class);
public GeoServerOAuth2ResourceServerAuthenticationFilter() {
super();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
LOGGER.log(Level.FINER, "Running filter.");
super.doFilter(request, response, chain);
}
@Override
public void initializeFromConfig(SecurityNamedServiceConfig pConfig) throws IOException {
LOGGER.log(Level.FINE, "Initializing filter.");
super.initializeFromConfig(pConfig);
}
@Override
public boolean applicableForHtml() {
return true;
}
@Override
public boolean applicableForServices() {
return true;
}
}

View File

@ -0,0 +1,171 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.resourceserver;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import java.util.List;
import javax.servlet.Filter;
import org.geoserver.config.GeoServer;
import org.geoserver.config.util.XStreamPersister;
import org.geoserver.logging.LoggingUtils;
import org.geoserver.platform.ContextLoadedEvent;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.security.GeoServerRoleConverter;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource;
import org.geoserver.security.config.SecurityNamedServiceConfig;
import org.geoserver.security.filter.AbstractFilterProvider;
import org.geoserver.security.filter.GeoServerCompositeFilter;
import org.geoserver.security.filter.GeoServerRoleResolvers;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverContext;
import org.geoserver.security.filter.GeoServerSecurityFilter;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfigValidator;
import org.geoserver.security.validation.SecurityConfigValidator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
/**
* Provider for {@link GeoServerOAuth2ResourceServerAuthenticationFilter}.
*
* <p>Used for the "Resource Server" use case. Implementation is unfinished, because a different GS extension supports
* this case already. Filter is not offered in UI. This code is never executed.
*
* @author awaterme
*/
public class GeoServerOAuth2ResourceServerAuthenticationProvider extends AbstractFilterProvider
implements ApplicationListener<ApplicationEvent> {
/** Filter types required for GeoServer */
private static final List<Class<?>> REQ_FILTER_TYPES = asList(BearerTokenAuthenticationFilter.class);
private class FilterBuilder {
private GeoServerOAuth2ResourceServerFilterConfig config;
private HttpSecurity http;
/**
* @param pConfig
* @param pHttpSecurity
*/
public FilterBuilder(GeoServerOAuth2ResourceServerFilterConfig pConfig, HttpSecurity pHttpSecurity) {
super();
config = pConfig;
http = pHttpSecurity;
}
private List<Filter> createFilters() {
try {
return createFiltersImpl();
} catch (Exception e) {
throw new RuntimeException("Failed to create OpenID filter.", e);
}
}
private List<Filter> createFiltersImpl() throws Exception {
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter;
if (redirectAuto) {
http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated());
}
JwtDecoder lDecoder = JwtDecoders.fromIssuerLocation(config.getIssuerUri());
ResolverContext lRoleResolverCtx = createRoleResolverContext();
jwtAuthenticationConverter = new GeoServerJwtAuthenticationConverter(lRoleResolverCtx);
OAuth2ResourceServerConfigurer<HttpSecurity> oauthConfig = http.oauth2ResourceServer();
oauthConfig.jwt(jwtConfig -> {
jwtConfig.decoder(lDecoder);
jwtConfig.jwtAuthenticationConverter(jwtAuthenticationConverter);
});
SecurityFilterChain lChain = http.build();
List<Filter> lFilters = lChain.getFilters();
lFilters = lFilters.stream()
.filter(f -> REQ_FILTER_TYPES.contains(f.getClass()))
.collect(toList());
return lFilters;
}
private ResolverContext createRoleResolverContext() {
GeoServerRoleConverter lConverter = null;
if (PreAuthenticatedUserNameRoleSource.Header.equals(config.getRoleSource())) {
String converterName = config.getRoleConverterName();
lConverter = GeoServerRoleResolvers.loadConverter(converterName);
}
return new GeoServerRoleResolvers.DefaultResolverContext(
securityManager,
config.getRoleServiceName(),
config.getUserGroupServiceName(),
config.getRolesHeaderAttribute(),
lConverter,
config.getRoleSource());
}
}
private GeoServerSecurityManager securityManager;
private ApplicationContext context;
private boolean redirectAuto = false;
public GeoServerOAuth2ResourceServerAuthenticationProvider(GeoServerSecurityManager pSecurityManager) {
assert pSecurityManager != null;
this.securityManager = pSecurityManager;
context = pSecurityManager.getApplicationContext();
assert context != null;
}
@Override
public void configure(XStreamPersister xp) {
xp.getXStream().alias("oauth2ResourceServerAuthentication", GeoServerOAuth2ResourceServerFilterConfig.class);
}
@Override
public Class<? extends GeoServerSecurityFilter> getFilterClass() {
return GeoServerOAuth2ResourceServerAuthenticationFilter.class;
}
@Override
public GeoServerSecurityFilter createFilter(SecurityNamedServiceConfig config) {
GeoServerOAuth2ResourceServerFilterConfig lConfig = (GeoServerOAuth2ResourceServerFilterConfig) config;
HttpSecurity httpSecurity = context.getBean(HttpSecurity.class);
FilterBuilder lBuilder = new FilterBuilder(lConfig, httpSecurity);
List<Filter> lFilters = lBuilder.createFilters();
GeoServerCompositeFilter filter = new GeoServerOAuth2ResourceServerAuthenticationFilter();
filter.setNestedFilters(lFilters);
return filter;
}
@Override
public SecurityConfigValidator createConfigurationValidator(GeoServerSecurityManager securityManager) {
return new GeoServerOAuth2LoginFilterConfigValidator(securityManager);
}
/**
* Provide a helpful OIDC_LOGGING configuration for this extension on context load event.
*
* @param event application event, responds ContextLoadEvent
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextLoadedEvent) {
// provide a helpful logging config for this extension
GeoServer geoserver = GeoServerExtensions.bean(GeoServer.class, this.context);
GeoServerResourceLoader loader = geoserver.getCatalog().getResourceLoader();
LoggingUtils.checkBuiltInLoggingConfiguration(loader, "OIDC_LOGGING");
}
}
}

View File

@ -0,0 +1,44 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.resourceserver;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig;
import org.geoserver.security.config.SecurityAuthFilterConfig;
/**
* Configuration for {@link GeoServerOAuth2ResourceServerAuthenticationFilter}.
*
* <p>Used for the "Resource Server" use case. Implementation is unfinished, because a different GS extension supports
* this case already. Filter is not offered in UI. This code is never executed.
*
* @author awaterme
*/
public class GeoServerOAuth2ResourceServerFilterConfig extends PreAuthenticatedUserNameFilterConfig
implements SecurityAuthFilterConfig {
private static final long serialVersionUID = -8581346584859849111L;
/**
* Add extra logging. NOTE: this might spill confidential information to the log - do not turn on in normal
* operation!
*/
boolean allowUnSecureLogging = false;
private String issuerUri;
public GeoServerOAuth2ResourceServerFilterConfig() {
super();
}
/** @return the issuerUri */
public String getIssuerUri() {
return issuerUri;
}
/** @param pIssuerUri the issuerUri to set */
public void setIssuerUri(String pIssuerUri) {
issuerUri = pIssuerUri;
}
}

View File

@ -0,0 +1,27 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.resourceserver;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException;
import org.geoserver.security.validation.FilterConfigValidator;
/**
* Validator for {@link GeoServerOAuth2ResourceServerAuthenticationFilter}.
*
* <p>Used for the "Resource Server" use case. Implementation is unfinished, because a different GS extension supports
* this case already. Filter is not offered in UI. This code is never executed.
*/
public class GeoServerOAuth2ResourceServerFilterConfigValidator extends FilterConfigValidator {
public GeoServerOAuth2ResourceServerFilterConfigValidator(GeoServerSecurityManager securityManager) {
super(securityManager);
}
@Override
protected GeoServerOAuth2FilterConfigException createFilterException(String errorid, Object... args) {
return new GeoServerOAuth2FilterConfigException(errorid, args);
}
}

View File

@ -0,0 +1,77 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.spring;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_OIDC;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.REGISTRATION_ID;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Logger;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geotools.util.logging.Logging;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder;
import org.springframework.util.Assert;
/**
* Adapts {@link OAuth2AuthorizationRequest}s to specific needs. Currently:
*
* <ul>
* <li>"response_mode" special case
* <li>PKCE
* </ul>
*
* @author awaterme
*/
public class GeoServerAuthorizationRequestCustomizer implements Consumer<OAuth2AuthorizationRequest.Builder> {
private static final Logger LOGGER = Logging.getLogger(GeoServerAuthorizationRequestCustomizer.class);
private GeoServerOAuth2LoginFilterConfig config;
/** @param pConfig */
public GeoServerAuthorizationRequestCustomizer(GeoServerOAuth2LoginFilterConfig pConfig) {
super();
config = pConfig;
Assert.notNull(pConfig, "configuration must not be null");
}
@Override
public void accept(Builder pBuilder) {
Consumer<Map<String, Object>> lCustomizer = attr -> {
Object lRegId = attr.get(REGISTRATION_ID);
boolean lIsOidc = REG_ID_OIDC.equals(lRegId);
boolean lIsOidcUsePKCE = config.isOidcUsePKCE();
// Google, GitHub and Azure support PKCE, OIDC depends on configuration
if (!lIsOidc || (lIsOidc && lIsOidcUsePKCE)) {
applyPKCE(pBuilder);
}
// ResponseMode: only for OIDC
String lResponseMode = config.getOidcResponseMode();
boolean lIsOidcRespMode = lResponseMode != null && !lResponseMode.isBlank();
if (lIsOidc && lIsOidcRespMode) {
applyResponseModeParam(pBuilder);
}
};
pBuilder.attributes(lCustomizer);
}
/** @param pBuilder */
private void applyPKCE(Builder pBuilder) {
Consumer<Builder> lConsumer = OAuth2AuthorizationRequestCustomizers.withPkce();
lConsumer.accept(pBuilder);
}
private void applyResponseModeParam(Builder pBuilder) {
String lResponseMode = config.getOidcResponseMode();
String lMode = lResponseMode.trim();
LOGGER.fine("Adding 'response_mode' parameter to authorize request: '" + lMode + "'.");
pBuilder.additionalParameters(m -> m.put("response_mode", lMode));
}
}

View File

@ -0,0 +1,44 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.spring;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_OIDC;
import java.util.function.Function;
import org.geoserver.security.oauth2.common.JwsAlgorithmNameParser;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
/**
* Determines the JWT token algorithm based on the {@link GeoServerOAuth2LoginFilterConfig}.
*
* @author awaterme
*/
public class GeoServerJwsAlgorithmResolver implements Function<ClientRegistration, JwsAlgorithm> {
private GeoServerOAuth2LoginFilterConfig configuration;
/** @param pConfiguration */
public GeoServerJwsAlgorithmResolver(GeoServerOAuth2LoginFilterConfig pConfiguration) {
super();
configuration = pConfiguration;
}
@Override
public JwsAlgorithm apply(ClientRegistration pClientReg) {
JwsAlgorithm lAlg = null;
if (REG_ID_OIDC.equals(pClientReg.getRegistrationId())) {
String lName = configuration.getOidcJwsAlgorithmName();
lAlg = new JwsAlgorithmNameParser().parse(lName);
}
if (lAlg == null) {
// also spring default
lAlg = SignatureAlgorithm.RS256;
}
return lAlg;
}
}

View File

@ -0,0 +1,168 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.spring;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_OIDC;
import static org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames.ID_TOKEN;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.geoserver.security.oauth2.common.ConfidentialLogger;
import org.geotools.util.logging.Logging;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.util.Assert;
/**
* {@link OAuth2AccessTokenResponseClient} allows to log confidential access token details to support trouble shooting
* OIDC providers.
*
* @author awaterme
*/
public class GeoServerOAuth2AccessTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
/**
* @param pDelegate
* @param pJwtDecoderFactory
*/
public GeoServerOAuth2AccessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> pDelegate,
JwtDecoderFactory<ClientRegistration> pJwtDecoderFactory) {
super();
delegate = pDelegate;
jwtDecoderFactory = pJwtDecoderFactory;
Assert.notNull(delegate, "delegate must not be null");
Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory must not be null");
}
private static Logger LOGGER = Logging.getLogger(GeoServerOAuth2AccessTokenResponseClient.class);
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> delegate;
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest pRequest) {
OAuth2AccessTokenResponse lTokenResponse;
try {
lTokenResponse = delegate.getTokenResponse(pRequest);
} catch (RuntimeException e) {
LOGGER.log(Level.WARNING, "Error obtaining token response.", e);
throw e;
}
try {
debugLog(pRequest, lTokenResponse);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error collecting data for logging.", e);
}
return lTokenResponse;
}
private void debugLog(OAuth2AuthorizationCodeGrantRequest pRequest, OAuth2AccessTokenResponse lTokenResponse) {
if (!ConfidentialLogger.isLoggable(Level.FINE)) {
return;
}
boolean lIsCommonProvider = false;
ClientRegistration lClientReg = pRequest.getClientRegistration();
String lRegId = lClientReg.getRegistrationId();
if (!REG_ID_OIDC.equals(lRegId)) {
lIsCommonProvider = true;
}
OAuth2AuthorizationExchange lExchange = pRequest.getAuthorizationExchange();
OAuth2AuthorizationResponse lAuthResp = lExchange.getAuthorizationResponse();
OAuth2AccessToken lAccessToken = lTokenResponse.getAccessToken();
String lAuthCode = lAuthResp.getCode();
OAuth2AccessToken.TokenType lType = lAccessToken.getTokenType();
Set<String> lScopes = lAccessToken.getScopes();
Map<String, Object> lAdditionals = new HashMap<>(lTokenResponse.getAdditionalParameters());
String lTokenValue = lAccessToken.getTokenValue();
Jwt lTokenJwt = null;
if (lTokenValue != null && lTokenValue.indexOf(".") > 0) {
lTokenJwt = parseToken(lClientReg, lTokenResponse, lTokenValue);
}
String lIdTokenValue = (String) lAdditionals.get(ID_TOKEN);
Jwt lIdToken = null;
if (lIdTokenValue != null) {
lAdditionals.remove(ID_TOKEN);
lIdToken = parseToken(lClientReg, lTokenResponse, lIdTokenValue);
}
if (lIsCommonProvider) {
// common: omit some confidential values not required for attribute analysis
String lMsg = "Access token received from {0} with accessTokenType={1}, "
+ "scopes={2}, accessTokenHeaders={3}, "
+ "accessTokenClaims={4}, additionalParameters={5}, "
+ "idTokenHeaders={6}, idTokenClaims={7}";
String[] lParams = new String[8];
lParams[0] = lRegId;
lParams[1] = lType == null ? null : lType.getValue();
lParams[2] = lScopes.stream().collect(Collectors.joining(","));
lParams[3] = lTokenJwt == null ? null : lTokenJwt.getHeaders().toString();
lParams[4] = lTokenJwt == null ? null : lTokenJwt.getClaims().toString();
lParams[5] = lAdditionals.toString();
lParams[6] = lIdToken == null ? null : lIdToken.getHeaders().toString();
lParams[7] = lIdToken == null ? null : lIdToken.getClaims().toString();
ConfidentialLogger.log(Level.FINE, lMsg, lParams);
} else {
String lMsg = "Access token received for {0} with authorizationCode={1}, accessTokenType={2}, "
+ "scopes={3}, accessTokenValue={4}, accessTokenHeaders={5}, "
+ "accessTokenClaims={6}, additionalParameters={7}, idTokenValue={8}, "
+ "idTokenHeaders={9}, idTokenClaims={10}";
String[] lParams = new String[11];
lParams[0] = lRegId;
lParams[1] = lAuthCode;
lParams[2] = lType == null ? null : lType.getValue();
lParams[3] = lScopes.stream().collect(Collectors.joining(","));
lParams[4] = tokenValueString(lTokenValue);
lParams[5] = lTokenJwt == null ? null : lTokenJwt.getHeaders().toString();
lParams[6] = lTokenJwt == null ? null : lTokenJwt.getClaims().toString();
lParams[7] = lAdditionals.toString();
lParams[8] = tokenValueString(lIdTokenValue);
lParams[9] = lIdToken == null ? null : lIdToken.getHeaders().toString();
lParams[10] = lIdToken == null ? null : lIdToken.getClaims().toString();
ConfidentialLogger.log(Level.FINE, lMsg, lParams);
}
}
private String tokenValueString(String pTokenValue) {
if (pTokenValue == null) {
return null;
}
String[] lSplitted = pTokenValue.split(Pattern.quote("."));
if (lSplitted.length == 3) {
return "body:" + lSplitted[1];
}
return "opaque:" + pTokenValue;
}
private Jwt parseToken(
ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse, String pValue) {
try {
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
Jwt jwt = jwtDecoder.decode(pValue);
return jwt;
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error parsing token data for logging.", e);
return null;
}
}
}

View File

@ -0,0 +1,73 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.spring;
import static java.lang.String.format;
import static java.util.stream.Collectors.joining;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geotools.util.logging.Logging;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* {@link OAuth2TokenValidator} implementation uses a {@link #delegate} for token validation. If OIDC token validation
* is configured to be non-enforcing, the validation result will always be "success." In such cases, if the delegate
* validation fails, only a warning message will be logged.
*
* @author awaterme
*/
public class GeoServerOidcConfigurableTokenValidator implements OAuth2TokenValidator<Jwt> {
private static Logger LOGGER = Logging.getLogger(GeoServerOidcConfigurableTokenValidator.class);
private GeoServerOAuth2LoginFilterConfig config;
private OAuth2TokenValidator<Jwt> delegate;
/**
* @param pConfig
* @param pDelegate
*/
public GeoServerOidcConfigurableTokenValidator(
GeoServerOAuth2LoginFilterConfig pConfig, OAuth2TokenValidator<Jwt> pDelegate) {
super();
config = pConfig;
delegate = pDelegate;
Assert.notNull(config, "configuration must not be null");
Assert.notNull(delegate, "delegate must not be null");
}
@Override
public OAuth2TokenValidatorResult validate(Jwt pToken) {
OAuth2TokenValidatorResult lResult = delegate.validate(pToken);
if (config.isOidcEnforceTokenValidation()) {
return lResult;
}
if (lResult.hasErrors()) {
int lCount = lResult.getErrors().size();
String lTxt = format("OIDC token validation failed with %d errors.", lCount);
StringBuilder lBuilder = new StringBuilder(lTxt);
if (LOGGER.isLoggable(Level.FINE)) {
lBuilder.append(" ")
.append(lResult.getErrors().stream()
.map(e -> e.toString())
.collect(joining()));
}
LOGGER.log(Level.WARNING, lBuilder.toString());
}
return OAuth2TokenValidatorResult.success();
}
/** @return the config */
public GeoServerOAuth2LoginFilterConfig getConfiguration() {
return config;
}
}

View File

@ -0,0 +1,46 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.spring;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
/**
* GeoServer factory for OIDC token decoding allows to replace the default Spring {@link OidcIdTokenDecoderFactory}.
* Required to support reconfiguration through the GS admin UI. The {@link #delegate} has to be replaced to use empty
* stale caches.
*
* @author awaterme
*/
public class GeoServerOidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
private volatile OidcIdTokenDecoderFactory delegate;
@Override
public JwtDecoder createDecoder(ClientRegistration pContext) {
if (delegate == null) {
throw new IllegalStateException("Decoder creation failed. Required configuration is missing.");
}
return delegate.createDecoder(pContext);
}
public void setGeoServerOAuth2LoginFilterConfig(GeoServerOAuth2LoginFilterConfig pConfig) {
if (pConfig == null) {
throw new IllegalArgumentException("Configuration must not be null");
}
OidcIdTokenDecoderFactory lFactory = new OidcIdTokenDecoderFactory();
lFactory.setJwsAlgorithmResolver(new GeoServerJwsAlgorithmResolver(pConfig));
lFactory.setJwtValidatorFactory(new GeoServerOidcIdTokenValidatorFactory(pConfig));
delegate = lFactory;
}
/** @return the delegate */
public OidcIdTokenDecoderFactory getDelegate() {
return delegate;
}
}

View File

@ -0,0 +1,49 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.spring;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_OIDC;
import java.util.function.Function;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenValidator;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.util.Assert;
/**
* Factory creates a {@link GeoServerOidcConfigurableTokenValidator} for the OIDC provider.
*
* @author awaterme
*/
public class GeoServerOidcIdTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
private GeoServerOAuth2LoginFilterConfig config;
/** @param pConfig */
public GeoServerOidcIdTokenValidatorFactory(GeoServerOAuth2LoginFilterConfig pConfig) {
super();
config = pConfig;
Assert.notNull(config, "configuration must not be null");
}
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration pClientReg) {
// src:
// org.springframework.security.oauth2.client.oidc.authentication.DefaultOidcIdTokenValidatorFactory
OAuth2TokenValidator<Jwt> lDefaultValidator =
new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(), new OidcIdTokenValidator(pClientReg));
String pRegId = pClientReg.getRegistrationId();
if (!REG_ID_OIDC.equals(pRegId)) {
return lDefaultValidator;
}
return new GeoServerOidcConfigurableTokenValidator(config, lDefaultValidator);
}
}

View File

@ -0,0 +1,31 @@
#
# (c) 2023 Open Source Geospatial Foundation - all rights reserved
# This code is licensed under the GPL 2.0 license, available at the root
# application directory.
#
#
GeoServerOAuth2FilterConfigException.OAUTH2_WKTS_URL_MALFORMED=Syntax error in JSON Web Key Set URI
GeoServerOAuth2FilterConfigException.OAUTH2_CHECKTOKEN_OR_WKTS_ENDPOINT_URL_REQUIRED=OAuth2 Check Token URI or WKTS URI required
GeoServerOAuth2FilterConfigException.OAUTH2_SCOPE_DELIMITER_MIXED=Scopes must be delimited by either space or comma, but not a mixture of both for {0}.
GeoServerOAuth2FilterConfigException.OAUTH2_MULTIPLE_INSTANCE_NOT_SUPPORTED=An OAuth2 / OpenID Connect filter is already installed: {0}. Multiple instances are currently not supported.
GeoServerOAuth2FilterConfigException.OAUTH2_URL_IN_LOGOUT_URI_MALFORMED=Syntax error in URI for OAuth2 logout page
GeoServerOAuth2FilterConfigException.OAUTH2_CHECKTOKENENDPOINT_URL_REQUIRED=OAuth2 Check Token URI required
GeoServerOAuth2FilterConfigException.OAUTH2_CHECKTOKENENDPOINT_URL_MALFORMED=OAuth2 Check Token URI syntax error
GeoServerOAuth2FilterConfigException.OAUTH2_ACCESSTOKENURI_MALFORMED=Missing or invalid Access Token URI
GeoServerOAuth2FilterConfigException.OAUTH2_ACCESSTOKENURI_NOT_HTTPS=Access Token URI must use HTTPS
GeoServerOAuth2FilterConfigException.OAUTH2_USERAUTHURI_MALFORMED=Missing or invalid User Authorization URI
GeoServerOAuth2FilterConfigException.OAUTH2_USERAUTHURI_NOT_HTTPS=User Authorization URI must use HTTPS
GeoServerOAuth2FilterConfigException.OAUTH2_REDIRECT_URI_MALFORMED=Syntax error in Redirect URI
GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_ID_REQUIRED=Client ID is required for {0}.
GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_SECRET_REQUIRED=Client Secret is required for {0}.
GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_USER_NAME_REQUIRED=User name attribute is required for {0}.
GeoServerOAuth2FilterConfigException.OAUTH2_SCOPE_REQUIRED=Scope(s) is required for {0}.
GeoServerOAuth2FilterConfigException.OAUTH2_URI_REQUIRED={0} URI is required for {1}.
GeoServerOAuth2FilterConfigException.OAUTH2_URI_INVALID={0} URI is invalid for {1}.
GeoServerOAuth2FilterConfigException.AEP_DENIED_WRONG_PROVIDER_COUNT=Skipping GeoServer login dialog is only possible with exactly one active provider.
GeoServerOAuth2FilterConfigException.MSGRAPH_COMBINATION_INVALID=The role source Microsoft Graph API is only supported with Microsoft Azure as login provider.
GeoServerOAuth2FilterConfigException.ROLE_SOURCE_ID_TOKEN_INVALID_FOR_GITHUB=The role source ID Token is not supported in combination with GitHub.
GeoServerOAuth2FilterConfigException.OAUTH2_USER_INFO_URI_REQUIRED_NO_OIDC=User Info URI is required if the scopes do not include "openid". \
If your provider supports OpenID Connect instead of OAuth2 only, consider adding "openid" to the scopes. Otherwise, ensure that the User Info URI is specified.
GeoServerOAuth2FilterConfigException.ROLE_SOURCE_USER_INFO_URI_REQUIRED=User Info URI is required when the role source is set to "User Info".
GeoServerOAuth2FilterConfigException.OAUTH2_JWK_SET_URI_REQUIRED=JSON Web Key Set URI is required.

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="OIDC_LOGGING" status="fatal" dest="out">
<Appenders>
<Console name="stdout" target="SYSTEM_OUT">
<PatternLayout pattern="%date{dd MMM HH:mm:ss} %-6level [%logger{2}] - %msg%n%throwable{filters(org.junit,org.apache.maven,sun.reflect,java.lang.reflect)}"/>
</Console>
<RollingFile name="geoserverlogfile">
<filename>logs/geoserver.log</filename>
<filePattern>logs/geoserver-%i.log</filePattern>
<PatternLayout pattern="%date{dd mmm HH:mm:ss} %-6level [%logger{2}] - %msg%n%throwable{filters(org.junit,org.apache.maven,sun.reflect,java.lang.reflect)}"/>
<Policies>
<SizeBasedTriggeringPolicy size="20 MB" />
</Policies>
<DefaultRolloverStrategy max="3" fileIndex="min"/>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="org.springframework" level="info"/>
<Logger name="org.geotools.factory" level="warn"/>
<Logger name="org.geotools" level="warn"/>
<Logger name="org.geowebcache" level="error"/>
<Logger name="org.geoserver" level="warn"/>
<Logger name="org.vfny.geoserver" level="warn"/>
<Logger name="org.geoserver.catalog" level="warn"/>
<Logger name="org.springframework.security" level="debug"/>
<!-- too much noise: -->
<Logger name="org.springframework.security.web.FilterChainProxy" level="info"/>
<Logger name="org.springframework.security.web.FilterChainProxy" level="info"/>
<Logger name="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter" level="warn"/>
<Logger name="org.springframework.security.web.context.HttpSessionSecurityContextRepository" level="info"/>
<Logger name="org.springframework.security.oauth2" level="debug"/>
<Logger name="org.geoserver.security" level="info"/>
<Logger name="org.geoserver.security.oauth2" level="trace"/>
<Logger name="org.geoserver.security.oauth2.common.ConfidentialLogger" level="trace"/>
<Root level="warn">
<AppenderRef ref="stdout"/>
<AppenderRef ref="geoserverlogfile"/>
</Root>
</Loggers>
</Configuration>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ~ (c) 2018 Open Source Geospatial Foundation - all rights reserved ~
This code is licensed under the GPL 2.0 license, available at the root ~
application directory. ~ -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.4.xsd
http://www.springframework.org/schema/security/oauth2
http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd">
<!-- Enable auto-wiring -->
<context:annotation-config />
<!-- Scan for auto-wiring classes in spring saml packages -->
<context:component-scan
base-package="org.geoserver.security.oauth2" />
<bean id="openIdConnectCoreExtension"
class="org.geoserver.platform.ModuleStatusImpl">
<constructor-arg index="0"
value="gs-sec-oidc-core" />
<constructor-arg index="1"
value="GeoServer Security OpenID Connect" />
</bean>
<bean id="oauth2LoginAuthenticationProvider"
depends-on="oidcHttpSecurityConfiguration"
class="org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationProvider">
<constructor-arg ref="authenticationManager" />
</bean>
<bean id="geoServerOAuth2LoginAuthenticationFilterBuilder"
class="org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilterBuilder" scope="prototype">
<property name="tokenDecoderFactory" ref="oidcIdTokenDecoderFactory"/>
<property name="http" ref="org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.httpSecurity"/>
</bean>
<!--
Used for the "Resource Server" use case. Implementation is unfinished, because a different GS
extension supports this case already. Filter is not offered in UI. This code is never executed.
<bean id="oauth2ResourceServerAuthenticationProvider"
depends-on="oidcHttpSecurityConfiguration"
class="org.geoserver.security.oauth2.resourceserver.GeoServerOAuth2ResourceServerAuthenticationProvider">
<constructor-arg ref="authenticationManager" />
</bean>
-->
<bean id="oidcHttpSecurityConfiguration"
class="org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration"
depends-on="contentNegotiationStrategy,oidcObjectPostProcessorConfiguration,oidcAuthenticationConfiguration" />
<!-- Attention: Name has to match the setter of HttpSecurityConfiguration,
otherwise no unique bean is found for autowiring -->
<bean id="contentNegotiationStrategy"
class="org.springframework.web.accept.HeaderContentNegotiationStrategy"></bean>
<bean id="oidcObjectPostProcessorConfiguration"
class="org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration" />
<bean id="oidcAuthenticationConfiguration"
class="org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration" />
<bean id="oidcIdTokenDecoderFactory" class="org.geoserver.security.oauth2.spring.GeoServerOidcIdTokenDecoderFactory"/>
</beans>

View File

@ -0,0 +1,27 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import static org.junit.Assert.assertEquals;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Test;
/** Tests for {@link ConfidentialLogger} */
public class ConfidentialLoggerTest {
/** Ensure adjusting Spring levels does not cause errors */
@Test
public void testEnableDisable() {
Logger logger = LogManager.getLogger("org.springframework.web.HttpLogging");
Level lOrgLevel = logger.getLevel();
ConfidentialLogger.setEnabled(true);
assertEquals(logger.getLevel(), Level.DEBUG);
ConfidentialLogger.setEnabled(false);
assertEquals(logger.getLevel(), lOrgLevel);
}
}

View File

@ -0,0 +1,302 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
/** */
package org.geoserver.security.oauth2.common;
import static java.time.Instant.now;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonMap;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_MICROSOFT;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.security.GeoServerRoleConverter;
import org.geoserver.security.GeoServerRoleService;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.filter.GeoServerRoleResolvers.DefaultResolverContext;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverParam;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.oauth2.common.GeoServerOAuth2RoleResolver.OAuth2ResolverParam;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
/** Tests {@link GeoServerOAuth2RoleResolver}. */
public class GeoServerOAuth2RoleResolverTest {
private static final String ROLES_CLAIM_NAME = "roles";
private static final String ROLE_NAME_AUTHENTICATED = "ROLE_AUTHENTICATED";
private static final String PRINCIPAL_NAME = "james";
private GeoServerSecurityManager mockSecurityManager = mock(GeoServerSecurityManager.class);
private GeoServerRoleConverter mockRoleConverter = mock(GeoServerRoleConverter.class);
private HttpServletRequest mockRequest = mock(HttpServletRequest.class);
private ClientRegistration mockClientReg = mock(ClientRegistration.class);
private GeoServerRoleService mockRoleService = mock(GeoServerRoleService.class);
private GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
private DefaultResolverContext context = newResolverContext(OpenIdRoleSource.AccessToken);
private OAuth2AccessToken accessToken =
new OAuth2AccessToken(TokenType.BEARER, "tokenValue", now(), now().plusMillis(1));
private OAuth2UserRequest userRequest = new OAuth2UserRequest(mockClientReg, accessToken);
private GeoServerOAuth2RoleResolver sut = new GeoServerOAuth2RoleResolver(config);
@Before
public void setUp() {
when(mockSecurityManager.getActiveRoleService()).thenReturn(mockRoleService);
config.setTokenRolesClaim(ROLES_CLAIM_NAME);
}
/** Verifies that parameter is checked for expected type */
@Test(expected = IllegalArgumentException.class)
public void testInvalidParameter() throws Exception {
ResolverParam lParam = new ResolverParam(PRINCIPAL_NAME, mockRequest, context);
sut.convert(lParam);
}
/**
* Verifies that users named "admin" or "root" at identity provider do not receive any roles to prevent from
* accidental local admin access.
*/
@Test
public void testGetRolesIsEmptyForGsLocalAdmins() {
for (String lName : new String[] {"admin", "root"}) {
// given
OAuth2ResolverParam lParam = new OAuth2ResolverParam(lName, mockRequest, context, userRequest);
// when
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertTrue("Expecting no roles for " + lName, lRoles.isEmpty());
}
}
/** Verifies that extracting roles from access token works as expected when claim is missing */
@Test
public void testGetRolesFromAccessTokenWithNoneExistingClaim() {
// given
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
// when
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED)));
}
/** Verifies that extracting roles from access token works as expected when claim is list of strings */
@Test
public void testGetRolesFromAccessTokenWithExistingClaim() {
// given
userRequest = new OAuth2UserRequest(
mockClientReg, accessToken, singletonMap(ROLES_CLAIM_NAME, Arrays.asList("ROLE1", "ROLE2")));
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
// when
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE1"), equalTo("ROLE2")));
}
/** Verifies that extracting roles from access token works as expected when claim is simple string */
@Test
public void testGetRolesFromAccessTokenWithExistingClaimSimpleString() {
// given
userRequest = new OAuth2UserRequest(mockClientReg, accessToken, singletonMap(ROLES_CLAIM_NAME, "ROLE1"));
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
// when
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE1")));
}
/** Verifies that extracting roles from access token works as expected when claim is string array */
@Test
public void testGetRolesFromAccessTokenWithExistingClaimArray() {
// given
userRequest = new OAuth2UserRequest(
mockClientReg, accessToken, singletonMap(ROLES_CLAIM_NAME, new String[] {"ROLE1", "ROLE2"}));
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
// when
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE1"), equalTo("ROLE2")));
}
/** Verifies that extracting roles from access token works as expected when using scope as source */
@Test
public void testGetRolesFromAccessTokenScope() {
// given
config.setTokenRolesClaim("scope");
accessToken =
new OAuth2AccessToken(TokenType.BEARER, "tokenValue", now(), now().plusMillis(1), singleton("ROLE1"));
userRequest = new OAuth2UserRequest(mockClientReg, accessToken);
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
// when
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE1")));
}
/** Verifies that extracting roles from ID token works as expected */
@Test
public void testGetRolesFromIdToken() {
// given
context = newResolverContext(OpenIdRoleSource.IdToken);
var lToken = new OidcIdToken(
"tokenValue", now(), now().plusMillis(1), Collections.singletonMap(ROLES_CLAIM_NAME, "ROLE1"));
var lRequest = new OidcUserRequest(mockClientReg, accessToken, lToken);
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, lRequest);
// when
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE1")));
}
/** Verifies that extracting roles from userInfo service works as expected when using authorities as source */
@Test
@SuppressWarnings("unchecked")
public void testGetRolesFromUserInfoServiceAuthorities() {
// given
config.setTokenRolesClaim("authorities");
context = newResolverContext(OpenIdRoleSource.UserInfo);
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
OAuth2UserService<OAuth2UserRequest, OAuth2User> mock = mock(OAuth2UserService.class);
sut.setUserServiceSupplier(() -> mock);
DefaultOAuth2User lUser = new DefaultOAuth2User(
singleton(new SimpleGrantedAuthority("ROLE1")),
Map.of("principalName", PRINCIPAL_NAME),
"principalName");
// when
when(mock.loadUser(any())).thenReturn(lUser);
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE1")));
}
/** Verifies that extracting roles from userInfo service works as expected when using attributes as source */
@Test
@SuppressWarnings("unchecked")
public void testGetRolesFromUserInfoServiceAttributes() {
// given
context = newResolverContext(OpenIdRoleSource.UserInfo);
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
OAuth2UserService<OAuth2UserRequest, OAuth2User> mock = mock(OAuth2UserService.class);
sut.setUserServiceSupplier(() -> mock);
DefaultOAuth2User lUser = new DefaultOAuth2User(
singleton(new SimpleGrantedAuthority("ROLE1")),
Map.of("principalName", PRINCIPAL_NAME, ROLES_CLAIM_NAME, "ROLE2"),
"principalName");
// when
when(mock.loadUser(any())).thenReturn(lUser);
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE2")));
}
/**
* Verifies that extracting roles from MS Graph API is skipped if MS is not the current IDP.
*
* @throws IOException
*/
@Test
public void testGetRolesFromMsGraphAPIWithIdpNotMs() throws IOException {
// given
context = newResolverContext(OpenIdRoleSource.MSGraphAPI);
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
MSGraphRolesResolver mock = mock(MSGraphRolesResolver.class);
sut.setMsGraphRolesResolverSupplier(() -> mock);
List<String> lRoleNames = List.of("ROLE1", "ROLE2");
// when
when(mock.resolveRoles(any())).thenReturn(lRoleNames);
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED)));
verify(mock, times(0)).resolveRoles(any());
}
/**
* Verifies that extracting roles from MS Graph API is working as expected with clientId "MS".
*
* @throws IOException
*/
@Test
public void testGetRolesFromMsGraphAPIWithIdpMs() throws IOException {
// given
context = newResolverContext(OpenIdRoleSource.MSGraphAPI);
OAuth2ResolverParam lParam = new OAuth2ResolverParam(PRINCIPAL_NAME, mockRequest, context, userRequest);
MSGraphRolesResolver mock = mock(MSGraphRolesResolver.class);
sut.setMsGraphRolesResolverSupplier(() -> mock);
List<String> lRoleNames = List.of("ROLE1", "ROLE2");
// when
when(mockClientReg.getRegistrationId()).thenReturn(REG_ID_MICROSOFT);
when(mock.resolveRoles(any())).thenReturn(lRoleNames);
Collection<GeoServerRole> lRoles = sut.convert(lParam);
// then
assertThat(lRoles, containsInAnyOrder(equalTo(ROLE_NAME_AUTHENTICATED), equalTo("ROLE1"), equalTo("ROLE2")));
}
private DefaultResolverContext newResolverContext(RoleSource pRoleSource) {
return new DefaultResolverContext(
mockSecurityManager, "default", "default", null, mockRoleConverter, pRoleSource);
}
}

View File

@ -0,0 +1,131 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import static java.time.Instant.now;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.security.GeoServerRoleConverter;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.filter.GeoServerRoleResolvers.DefaultResolverContext;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverParam;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.oauth2.common.GeoServerOAuth2UserServices.GeoServerOAuth2UserService;
import org.geoserver.security.oauth2.common.GeoServerOAuth2UserServices.GeoServerOidcUserService;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
/** Tests for {@link GeoServerOAuth2UserServices} */
public class GeoServerOAuth2UserServicesTest {
private static final String USER_NAME_ATTR = "principal";
private HttpServletRequest mockRequest = mock(HttpServletRequest.class);
private GeoServerSecurityManager securityManager = mock(GeoServerSecurityManager.class);
private GeoServerRoleConverter roleConverter = mock(GeoServerRoleConverter.class);
private RoleSource roleSource = OpenIdRoleSource.AccessToken;
private ClientRegistration mockClientReg = mock(ClientRegistration.class);
private DefaultOAuth2UserService mockOAuth2UserService = mock(DefaultOAuth2UserService.class);
private OidcUserService mockOidcService = mock(OidcUserService.class);
private GeoServerOAuth2RoleResolver mockRoleResolver = mock(GeoServerOAuth2RoleResolver.class);
private ProviderDetails mockProviderDetails = mock(ProviderDetails.class);
private UserInfoEndpoint mockUserInfoEndpoint = mock(UserInfoEndpoint.class);
private Supplier<HttpServletRequest> requestSupplier = () -> mockRequest;
private DefaultResolverContext resolverContext =
new DefaultResolverContext(securityManager, "default", "default", null, roleConverter, roleSource);
private GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
@Before
public void setUp() {
when(mockClientReg.getProviderDetails()).thenReturn(mockProviderDetails);
when(mockProviderDetails.getUserInfoEndpoint()).thenReturn(mockUserInfoEndpoint);
when(mockUserInfoEndpoint.getUserNameAttributeName()).thenReturn(USER_NAME_ATTR);
}
/** Smoke test for {@link GeoServerOAuth2UserService}. */
@Test
public void testGeoServerOAuth2UserService() {
// given
OAuth2AccessToken lAccessToken =
new OAuth2AccessToken(TokenType.BEARER, "tokenValue", now(), now().plusMillis(1));
OAuth2UserRequest lUserRequest = new OAuth2UserRequest(mockClientReg, lAccessToken);
GeoServerOAuth2UserService sut = new GeoServerOAuth2UserService(resolverContext, requestSupplier, config);
sut.setDelegateSupplier(() -> mockOAuth2UserService);
sut.setResolverSupplier(() -> mockRoleResolver);
DefaultOAuth2User lDelgatesUser = new DefaultOAuth2User(
List.of(new SimpleGrantedAuthority("ROLE1")),
Map.of(USER_NAME_ATTR, "james", "attr1", "value1"),
USER_NAME_ATTR);
List<GeoServerRole> lResolversRoles = List.of(new GeoServerRole("R1"), new GeoServerRole("R2"));
// when
when(mockRoleResolver.convert(any(ResolverParam.class))).thenReturn(lResolversRoles);
when(mockOAuth2UserService.loadUser(any(OAuth2UserRequest.class))).thenReturn(lDelgatesUser);
OAuth2User lUser = sut.loadUser(lUserRequest);
// then
assertNotNull(lUser);
assertThat(lUser.getAuthorities(), containsInAnyOrder(equalTo("R1"), equalTo("R2")));
}
/** Smoke test for {@link GeoServerOidcUserService}. */
@Test
public void testGeoServerOidcUserService() {
// given
OAuth2AccessToken lAccessToken =
new OAuth2AccessToken(TokenType.BEARER, "tokenValue", now(), now().plusMillis(1));
OidcIdToken lIdToken = new OidcIdToken(
"idTokenValue", now(), now().plusMillis(1), Map.of("sub", "james@b.co", USER_NAME_ATTR, "james"));
OidcUserRequest lUserRequest = new OidcUserRequest(mockClientReg, lAccessToken, lIdToken);
GeoServerOidcUserService sut = new GeoServerOidcUserService(resolverContext, requestSupplier, config);
sut.setDelegateSupplier(() -> mockOidcService);
sut.setResolverSupplier(() -> mockRoleResolver);
DefaultOidcUser lDelgatesUser = new DefaultOidcUser(List.of(new SimpleGrantedAuthority("ROLE1")), lIdToken);
List<GeoServerRole> lResolversRoles = List.of(new GeoServerRole("R1"), new GeoServerRole("R2"));
// when
when(mockRoleResolver.convert(any(ResolverParam.class))).thenReturn(lResolversRoles);
when(mockOidcService.loadUser(any(OidcUserRequest.class))).thenReturn(lDelgatesUser);
OidcUser lUser = sut.loadUser(lUserRequest);
// then
assertNotNull(lUser);
assertThat(lUser.getAuthorities(), containsInAnyOrder(equalTo("R1"), equalTo("R2")));
}
}

View File

@ -0,0 +1,28 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.common;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
/** tests for {@link JwsAlgorithmNameParser}. */
public class JwsAlgorithmNameParserTest {
private JwsAlgorithmNameParser sut = new JwsAlgorithmNameParser();
@Test
public void testParse() {
assertNull(sut.parse("GeoServer"));
assertNull(sut.parse(null));
assertNull(sut.parse(" "));
assertNotNull(sut.parse("RS256"));
assertEquals("RS256", sut.parse("RS256").getName());
assertNotNull(sut.parse("HS256"));
assertEquals("HS256", sut.parse("HS256").getName());
}
}

View File

@ -0,0 +1,138 @@
/*
* (c) 2022 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*
*/
package org.geoserver.security.oauth2.common;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.List;
import org.junit.Test;
public class MSGraphRolesResolverTest {
@Test
public void testMSGraphAPIEndpoint() throws IOException {
MSGraphRolesResolver resolver = new MSGraphRolesResolver();
assertEquals("https://graph.microsoft.com/v1.0/me/memberOf", resolver.memberOfEndpoint.toString());
}
@Test
public void testHTTPConnection() throws IOException {
MSGraphRolesResolver resolver = new MSGraphRolesResolver();
// HttpURLConnection treats "Authorization" request header as private so we cannot verify
// it. We change its name so we can access it!
resolver.authorizationHeaderName = "AuthorizationZZZ";
HttpURLConnection http = resolver.createHTTPRequest("accesstoken");
try {
assertEquals(
"https://graph.microsoft.com/v1.0/me/memberOf",
http.getURL().toString());
assertEquals("Bearer accesstoken", http.getRequestProperty("AuthorizationZZZ"));
assertEquals("application/json", http.getRequestProperty("Accept"));
} finally {
if (http != null) http.disconnect();
}
}
// typical MSGraph response
String json1 = "{\n"
+ " \"@odata.context\": \"https://graph.microsoft.com/v1.0/$metadata#directoryObjects\",\n"
+ " \"value\": [\n"
+ " {\n"
+ " \"@odata.type\": \"#microsoft.graph.directoryRole\",\n"
+ " \"id\": \"fced5395-6be6-436c-a7f9-5c638cbdeb20\",\n"
+ " \"deletedDateTime\": null,\n"
+ " \"description\": null,\n"
+ " \"displayName\": null,\n"
+ " \"roleTemplateId\": null\n"
+ " },\n"
+ " {\n"
+ " \"@odata.type\": \"#microsoft.graph.group\",\n"
+ " \"id\": \"d93c6444-feee-4b67-8c0f-15d6796370cb\",\n"
+ " \"deletedDateTime\": null,\n"
+ " \"classification\": null,\n"
+ " \"createdDateTime\": \"2022-05-04T17:57:04Z\",\n"
+ " \"creationOptions\": [],\n"
+ " \"description\": \"geoserverAdmin\",\n"
+ " \"displayName\": \"geoserverAdmin\",\n"
+ " \"expirationDateTime\": null,\n"
+ " \"groupTypes\": [],\n"
+ " \"isAssignableToRole\": null,\n"
+ " \"mail\": null,\n"
+ " \"mailEnabled\": false,\n"
+ " \"mailNickname\": \"e93d884d-3\",\n"
+ " \"membershipRule\": null,\n"
+ " \"membershipRuleProcessingState\": null,\n"
+ " \"onPremisesDomainName\": null,\n"
+ " \"onPremisesLastSyncDateTime\": null,\n"
+ " \"onPremisesNetBiosName\": null,\n"
+ " \"onPremisesSamAccountName\": null,\n"
+ " \"onPremisesSecurityIdentifier\": null,\n"
+ " \"onPremisesSyncEnabled\": null,\n"
+ " \"preferredDataLocation\": null,\n"
+ " \"preferredLanguage\": null,\n"
+ " \"proxyAddresses\": [],\n"
+ " \"renewedDateTime\": \"2022-05-04T17:57:04Z\",\n"
+ " \"resourceBehaviorOptions\": [],\n"
+ " \"resourceProvisioningOptions\": [],\n"
+ " \"securityEnabled\": true,\n"
+ " \"securityIdentifier\": \"S-1-12-1-3644613700-1265106670-3591704460-3413140345\",\n"
+ " \"theme\": null,\n"
+ " \"visibility\": null,\n"
+ " \"onPremisesProvisioningErrors\": []\n"
+ " },\n"
+ " {\n"
+ " \"@odata.type\": \"#microsoft.graph.group\",\n"
+ " \"id\": \"3a94275f-7d53-4205-8d78-11f39e9ffa5a\",\n"
+ " \"deletedDateTime\": null,\n"
+ " \"classification\": null,\n"
+ " \"createdDateTime\": \"2022-05-18T20:21:11Z\",\n"
+ " \"creationOptions\": [],\n"
+ " \"description\": \"geonetworkAdmin\",\n"
+ " \"displayName\": \"geonetworkAdmin\",\n"
+ " \"expirationDateTime\": null,\n"
+ " \"groupTypes\": [],\n"
+ " \"isAssignableToRole\": null,\n"
+ " \"mail\": null,\n"
+ " \"mailEnabled\": false,\n"
+ " \"mailNickname\": \"52fa2d5e-5\",\n"
+ " \"membershipRule\": null,\n"
+ " \"membershipRuleProcessingState\": null,\n"
+ " \"onPremisesDomainName\": null,\n"
+ " \"onPremisesLastSyncDateTime\": null,\n"
+ " \"onPremisesNetBiosName\": null,\n"
+ " \"onPremisesSamAccountName\": null,\n"
+ " \"onPremisesSecurityIdentifier\": null,\n"
+ " \"onPremisesSyncEnabled\": null,\n"
+ " \"preferredDataLocation\": null,\n"
+ " \"preferredLanguage\": null,\n"
+ " \"proxyAddresses\": [],\n"
+ " \"renewedDateTime\": \"2022-05-18T20:21:11Z\",\n"
+ " \"resourceBehaviorOptions\": [],\n"
+ " \"resourceProvisioningOptions\": [],\n"
+ " \"securityEnabled\": true,\n"
+ " \"securityIdentifier\": \"S-1-12-1-982787935-1107656019-4078008461-1526374302\",\n"
+ " \"theme\": null,\n"
+ " \"visibility\": null,\n"
+ " \"onPremisesProvisioningErrors\": []\n"
+ " } \n"
+ " ]\n"
+ "}";
@Test
public void testParse() {
MSGraphRolesResolver resolver = new MSGraphRolesResolver();
List<String> groups = resolver.parseJson(json1);
assertEquals(2, groups.size());
assertEquals("d93c6444-feee-4b67-8c0f-15d6796370cb", groups.get(0));
assertEquals("3a94275f-7d53-4205-8d78-11f39e9ffa5a", groups.get(1));
}
}

View File

@ -0,0 +1,362 @@
/*
* (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.security.oauth2.login;
import static java.util.Collections.singleton;
import static org.apache.commons.lang3.reflect.FieldUtils.readField;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_GIT_HUB;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_GOOGLE;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_MICROSOFT;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_OIDC;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.Filter;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.oauth2.common.ConfidentialLogger;
import org.geoserver.security.oauth2.spring.GeoServerAuthorizationRequestCustomizer;
import org.geoserver.security.oauth2.spring.GeoServerOidcConfigurableTokenValidator;
import org.geoserver.security.oauth2.spring.GeoServerOidcIdTokenDecoderFactory;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer.AuthorizationEndpointConfig;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer.TokenEndpointConfig;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer.UserInfoEndpointConfig;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Tests {@link GeoServerOAuth2LoginAuthenticationFilterBuilder}
*
* @author awaterme
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public class GeoServerOAuth2LoginAuthenticationFilterBuilderTest {
private GeoServerOAuth2LoginFilterConfig configuration;
private GeoServerSecurityManager mockSecurityManager;
private HttpSecurity mockHttp;
private ApplicationEventPublisher mockEventPublisher;
private GeoServerOidcIdTokenDecoderFactory mockTokenDecoderFactory;
private OAuth2LoginConfigurer mockOAuth2LoginConfigurer;
private UserInfoEndpointConfig mockUserInfoConfig = mock(UserInfoEndpointConfig.class);
private AuthorizationEndpointConfig mockAuthorizationConfig = mock(AuthorizationEndpointConfig.class);
private TokenEndpointConfig mockTokenConfig = mock(TokenEndpointConfig.class);
private GeoServerOAuth2LoginAuthenticationFilterBuilder sut = new GeoServerOAuth2LoginAuthenticationFilterBuilder();
@Before
public void setupDependencies() throws Exception {
System.setProperty(GeoServerOAuth2LoginFilterConfig.OPENID_TEST_GS_PROXY_BASE, "http://localhost/geoserver");
configuration = new GeoServerOAuth2LoginFilterConfig();
mockSecurityManager = mock(GeoServerSecurityManager.class);
mockHttp = mock(HttpSecurity.class);
mockEventPublisher = mock(ApplicationEventPublisher.class);
mockTokenDecoderFactory = mock(GeoServerOidcIdTokenDecoderFactory.class);
mockOAuth2LoginConfigurer = mock(OAuth2LoginConfigurer.class);
when(mockOAuth2LoginConfigurer.userInfoEndpoint()).thenReturn(mockUserInfoConfig);
when(mockOAuth2LoginConfigurer.authorizationEndpoint()).thenReturn(mockAuthorizationConfig);
when(mockOAuth2LoginConfigurer.tokenEndpoint()).thenReturn(mockTokenConfig);
// when oauth2Login(): provide mock configurer
when(mockHttp.oauth2Login(any())).thenAnswer(stub -> {
Customizer<OAuth2LoginConfigurer<HttpSecurity>> lCallback = stub.getArgument(0, Customizer.class);
lCallback.customize(mockOAuth2LoginConfigurer);
return mockHttp;
});
}
private void assignDependencies() {
sut.setConfiguration(configuration);
sut.setSecurityManager(mockSecurityManager);
sut.setHttp(mockHttp);
sut.setEventPublisher(mockEventPublisher);
sut.setTokenDecoderFactory(mockTokenDecoderFactory);
}
/** when called with incomplete required dependencies: fail */
@Test(expected = IllegalArgumentException.class)
public void testCheckMissingDeps() {
sut.build();
}
/** when called with dependencies, without active provider: no exceptions */
@Test
public void testNoProviderActive() throws Exception {
// given:
assignDependencies();
// when: called without provider active
GeoServerOAuth2LoginAuthenticationFilter lFilter = sut.build();
// then: no exception, no filters
assertNotNull(lFilter);
assertEquals(0, lFilter.getNestedFilters().size());
assertNull(lFilter.getLogoutSuccessHandler());
// further build is not permitted
try {
sut.build();
fail("Exception IllegalArgumentException.");
} catch (IllegalArgumentException e) {
// expected
}
}
/**
* Verifies that the expected calls on the spring configuration API have occurred and filter chain is reconstructed
* as expected.
*
* @throws Exception
*/
@Test
public void testFilterConstructionWithGoogle() throws Exception {
// given
assignDependencies();
ClientRegistrationRepository lRepo = mock(ClientRegistrationRepository.class);
OAuth2AuthorizedClientService lService = mock(OAuth2AuthorizedClientService.class);
Filter f0 = mock(Filter.class);
Filter f1 = new OAuth2AuthorizationRequestRedirectFilter(lRepo);
Filter f2 = new OAuth2LoginAuthenticationFilter(lRepo, lService);
Filter f3 = new RequestCacheAwareFilter();
Filter f4 = mock(Filter.class);
List<Filter> lFilters = Arrays.asList(f0, f1, f2, f3, f4);
// * http returns a "complete" spring chain, here with some mock filters
when(mockHttp.build()).thenReturn(new DefaultSecurityFilterChain(mock(RequestMatcher.class), lFilters));
// * Google is active and setup
configuration.setGoogleEnabled(true);
configuration.setGoogleClientId("myClientId");
configuration.setGoogleClientSecret("myClientSecret");
// * skip GS login is enabled
configuration.setEnableRedirectAuthenticationEntryPoint(true);
// * unsecure logging is active
configuration.setOidcAllowUnSecureLogging(true);
ConfidentialLogger.setEnabled(false);
// when: building filter
GeoServerOAuth2LoginAuthenticationFilter lFilter = sut.build();
// then
// * logout handler must be in place
assertNotNull(lFilter.getLogoutSuccessHandler());
// * relevant filters are extracted
assertEquals(4, lFilter.getNestedFilters().size());
assertNotNull(sut.getRedirectToProviderFilter());
Assert.assertSame(f1, lFilter.getNestedFilters().get(0));
Assert.assertSame(f2, lFilter.getNestedFilters().get(1));
Assert.assertSame(f3, lFilter.getNestedFilters().get(2));
Assert.assertSame(
sut.getRedirectToProviderFilter(), lFilter.getNestedFilters().get(3));
// * configuration API has been called, and entire build process is through without nulls
verify(mockTokenDecoderFactory, times(1)).setGeoServerOAuth2LoginFilterConfig(isNotNull());
verify(mockHttp, times(1)).build();
verify(mockOAuth2LoginConfigurer, times(1)).clientRegistrationRepository(isNotNull());
verify(mockOAuth2LoginConfigurer, times(1)).authorizedClientRepository(isNotNull());
verify(mockOAuth2LoginConfigurer, times(1)).authorizedClientService(isNotNull());
verify(mockUserInfoConfig, times(1)).userService(isNotNull());
verify(mockUserInfoConfig, times(1)).oidcUserService(isNotNull());
verify(mockAuthorizationConfig, times(1)).authorizationRequestResolver(isNotNull());
verify(mockTokenConfig, times(1)).accessTokenResponseClient(isNotNull());
// * events have been published
verify(mockEventPublisher, times(4)).publishEvent(any(OAuth2LoginButtonEnablementEvent.class));
// * google client is setup as expected
ClientRegistrationRepository lClientRepo = sut.getClientRegistrationRepository();
assertNotNull(lClientRepo);
ClientRegistration lGoogleReg = lClientRepo.findByRegistrationId(REG_ID_GOOGLE);
assertNotNull(lGoogleReg);
assertEquals("myClientId", lGoogleReg.getClientId());
assertEquals("myClientSecret", lGoogleReg.getClientSecret());
assertTrue(ConfidentialLogger.isEnabled());
}
/**
* Tests OIDC client construction and verifies configured settings and GeoServer customizers are in place.
*
* @throws Exception
*/
@Test
public void testOidcConstruction() throws Exception {
// given
assignDependencies();
// * OIDC is used, with the respective settings
configuration.setOidcEnabled(true);
configuration.setOidcClientId("myId");
configuration.setOidcClientSecret("mySecret");
configuration.setOidcUserNameAttribute("myAttr");
configuration.setOidcRedirectUri("myRedirectUri");
configuration.setOidcScopes("myScopes");
configuration.setOidcDiscoveryUri("myDiscoveryUrik");
configuration.setOidcTokenUri("myTokenUri");
configuration.setOidcAuthorizationUri("myAuthorizationUri");
configuration.setOidcUserInfoUri("myUserInfoUri");
configuration.setOidcJwkSetUri("https://myJwkSetUri");
configuration.setOidcLogoutUri("myLogoutUri");
configuration.setOidcEnforceTokenValidation(false);
configuration.setOidcUsePKCE(true);
configuration.setOidcResponseMode("query");
configuration.setOidcAuthenticationMethodPostSecret(true);
configuration.setOidcAllowUnSecureLogging(false);
// * filter construction is tested in testFilterConstructionWithGoogle()
when(mockHttp.build())
.thenReturn(new DefaultSecurityFilterChain(mock(RequestMatcher.class), new ArrayList<>()));
// * builder uses real factory
sut.setTokenDecoderFactory(new GeoServerOidcIdTokenDecoderFactory());
// * confidential logger is enabled before
ConfidentialLogger.setEnabled(true);
// when
// * filter is constructed
GeoServerOAuth2LoginAuthenticationFilter lFilter = sut.build();
// then
// * filter was created
assertNotNull(lFilter);
// * settings where transmitted
ClientRegistrationRepository lClientRepo = sut.getClientRegistrationRepository();
assertNotNull(lClientRepo);
ClientRegistration lReg = lClientRepo.findByRegistrationId(REG_ID_OIDC);
assertNotNull(lReg);
assertEquals("myId", lReg.getClientId());
assertEquals("mySecret", lReg.getClientSecret());
assertEquals("myAttr", lReg.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
assertEquals("myRedirectUri", lReg.getRedirectUri());
assertEquals(singleton("myScopes"), lReg.getScopes());
assertEquals("myTokenUri", lReg.getProviderDetails().getTokenUri());
assertEquals("myAuthorizationUri", lReg.getProviderDetails().getAuthorizationUri());
assertEquals(
"myUserInfoUri", lReg.getProviderDetails().getUserInfoEndpoint().getUri());
assertEquals("https://myJwkSetUri", lReg.getProviderDetails().getJwkSetUri());
assertEquals(
"myLogoutUri",
lReg.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint"));
GeoServerOidcIdTokenDecoderFactory lTokenDecoderFactory = sut.getTokenDecoderFactory();
assertNotNull(lTokenDecoderFactory);
JwtDecoder lDecoder = lTokenDecoderFactory.createDecoder(lReg);
Object lValidatorObject = readField(lDecoder, "jwtValidator", true);
assertNotNull(lValidatorObject);
assertEquals(GeoServerOidcConfigurableTokenValidator.class, lValidatorObject.getClass());
GeoServerOidcConfigurableTokenValidator lValidator = (GeoServerOidcConfigurableTokenValidator) lValidatorObject;
// * enforceTokenValidation is addressed by validator
Assert.assertSame(configuration, lValidator.getConfiguration());
// * PKCE, extra request parameters (response mode)
DefaultOAuth2AuthorizationRequestResolver lResolver = sut.getAuthorizationRequestResolver();
assertNotNull(lResolver);
Object lCustomizerObject = readField(lResolver, "authorizationRequestCustomizer", true);
assertNotNull(lCustomizerObject);
assertEquals(GeoServerAuthorizationRequestCustomizer.class, lCustomizerObject.getClass());
// * authentication method post secret
assertEquals(CLIENT_SECRET_POST, lReg.getClientAuthenticationMethod());
// * insecure logging
assertFalse(ConfidentialLogger.isEnabled());
}
/**
* Verifies that the expected calls on the spring configuration API have occurred and filter chain is reconstructed
* as expected.
*
* @throws Exception
*/
@Test
public void testFilterConstructionWithFurtherProviders() throws Exception {
// given
assignDependencies();
ClientRegistrationRepository lRepo = mock(ClientRegistrationRepository.class);
OAuth2AuthorizedClientService lService = mock(OAuth2AuthorizedClientService.class);
Filter f0 = mock(Filter.class);
Filter f1 = new OAuth2AuthorizationRequestRedirectFilter(lRepo);
Filter f2 = new OAuth2LoginAuthenticationFilter(lRepo, lService);
Filter f3 = new RequestCacheAwareFilter();
Filter f4 = mock(Filter.class);
List<Filter> lFilters = Arrays.asList(f0, f1, f2, f3, f4);
// * http returns a "complete" spring chain, here with some mock filters
when(mockHttp.build()).thenReturn(new DefaultSecurityFilterChain(mock(RequestMatcher.class), lFilters));
// * GitHub is active and setup
configuration.setGitHubEnabled(true);
configuration.setGitHubClientId("ghClientId");
configuration.setGitHubClientSecret("ghClientSecret");
// * GitHub is active and setup
configuration.setMsEnabled(true);
configuration.setMsClientId("msClientId");
configuration.setMsClientSecret("msClientSecret");
// when: building filter
sut.build();
// * google client is setup as expected
ClientRegistrationRepository lClientRepo = sut.getClientRegistrationRepository();
assertNotNull(lClientRepo);
ClientRegistration lClientReg = lClientRepo.findByRegistrationId(REG_ID_GIT_HUB);
assertNotNull(lClientReg);
assertEquals("ghClientId", lClientReg.getClientId());
assertEquals("ghClientSecret", lClientReg.getClientSecret());
lClientReg = lClientRepo.findByRegistrationId(REG_ID_MICROSOFT);
assertNotNull(lClientReg);
assertEquals("msClientId", lClientReg.getClientId());
assertEquals("msClientSecret", lClientReg.getClientSecret());
}
}

View File

@ -0,0 +1,319 @@
/*
* (c) 2018 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*
*/
package org.geoserver.security.oauth2.login;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException.MSGRAPH_COMBINATION_INVALID;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException.OAUTH2_ACCESSTOKENURI_MALFORMED;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_SECRET_REQUIRED;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_USER_NAME_REQUIRED;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException.OAUTH2_URL_IN_LOGOUT_URI_MALFORMED;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException.OAUTH2_USERAUTHURI_MALFORMED;
import static org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException.OAUTH2_USERAUTHURI_NOT_HTTPS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
import org.geoserver.security.oauth2.common.GeoServerOAuth2FilterConfigException;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource;
import org.geoserver.security.validation.FilterConfigException;
import org.geoserver.test.GeoServerMockTestSupport;
import org.geotools.util.logging.Logging;
import org.junit.Before;
import org.junit.Test;
/** Tests for {@link GeoServerOAuth2LoginFilterConfigValidator} */
public class GeoServerOAuth2LoginFilterConfigValidatorTest extends GeoServerMockTestSupport {
protected static Logger LOGGER = Logging.getLogger("org.geoserver.security");
private GeoServerOAuth2LoginFilterConfigValidator validator;
@Before
public void setUp() {
validator = new GeoServerOAuth2LoginFilterConfigValidator(getSecurityManager());
System.setProperty(GeoServerOAuth2LoginFilterConfig.OPENID_TEST_GS_PROXY_BASE, "http://localhost/geoserver");
}
@Test
public void testOAuth2FilterConfigValidation() throws Exception {
GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
config.setOidcEnabled(true);
config.setClassName(GeoServerOAuth2LoginAuthenticationFilter.class.getName());
config.setName("testOAuth2");
check(config);
validator.validateOAuth2FilterConfig(config);
}
private void check(GeoServerOAuth2LoginFilterConfig config) throws Exception {
Callable<Void> lValidate = () -> {
validator.validateOAuth2FilterConfig(config);
fail("FilterConfigException expected.");
return null;
};
config.setOidcUserNameAttribute(null);
try {
lValidate.call();
} catch (FilterConfigException ex) {
assertExceptionCodeWithArgCount(ex, OAUTH2_CLIENT_USER_NAME_REQUIRED, 1);
}
config.setOidcUserNameAttribute("email");
// when: null uri
try {
lValidate.call();
} catch (FilterConfigException ex) {
// then: null not accepted
assertExceptionCodeWithArgCount(ex, OAUTH2_USERAUTHURI_MALFORMED, 0);
}
// when: invalid uri
config.setOidcAuthorizationUri("lalala");
try {
lValidate.call();
} catch (FilterConfigException ex) {
// then: invalid not accepted
assertExceptionCodeWithArgCount(ex, OAUTH2_USERAUTHURI_MALFORMED, 0);
}
// when: http
config.setOidcAuthorizationUri("http://lalala");
try {
lValidate.call();
} catch (FilterConfigException ex) {
// then: by default not accepted
assertExceptionCodeWithArgCount(ex, OAUTH2_USERAUTHURI_NOT_HTTPS, 0);
}
// when: http allowed and http used
config.setOidcForceAuthorizationUriHttps(false);
try {
lValidate.call();
} catch (FilterConfigException ex) {
// then: actual validation ok, next validation fails
assertExceptionCodeWithArgCount(ex, OAUTH2_ACCESSTOKENURI_MALFORMED, 0);
}
// when: http not allowed and https used
config.setOidcForceAuthorizationUriHttps(false);
config.setOidcAuthorizationUri("https://lalala");
config.setOidcForceAuthorizationUriHttps(false);
try {
lValidate.call();
} catch (FilterConfigException ex) {
// then: actual validation ok, next validation fails
assertExceptionCodeWithArgCount(ex, OAUTH2_ACCESSTOKENURI_MALFORMED, 0);
}
// when: access token URI Ok
config.setOidcTokenUri("https://tokenuri");
try {
lValidate.call();
} catch (FilterConfigException ex) {
// then: actual validation ok, next validation fails
assertExceptionCodeWithArgCount(ex, GeoServerOAuth2FilterConfigException.OAUTH2_CLIENT_ID_REQUIRED, 1);
}
config.setOidcClientId("myClientId");
try {
lValidate.call();
} catch (FilterConfigException ex) {
assertExceptionCodeWithArgCount(ex, OAUTH2_CLIENT_SECRET_REQUIRED, 1);
}
config.setOidcClientSecret("myClientSecret");
config.setOidcLogoutUri("blbla");
try {
lValidate.call();
} catch (GeoServerOAuth2FilterConfigException ex) {
assertExceptionCodeWithArgCount(ex, OAUTH2_URL_IN_LOGOUT_URI_MALFORMED, 0);
}
config.setOidcLogoutUri("http://localhost/gesoerver");
config.setOidcClientId("oauth2clientid");
config.setOidcClientSecret("oauth2clientsecret");
// when: scope openid removed
config.setOidcScopes("email,profile");
try {
lValidate.call();
} catch (GeoServerOAuth2FilterConfigException ex) {
assertExceptionCodeWithArgCount(
ex, GeoServerOAuth2FilterConfigException.OAUTH2_USER_INFO_URI_REQUIRED_NO_OIDC, 0);
}
config.setOidcScopes("openid,email,profile");
try {
lValidate.call();
} catch (GeoServerOAuth2FilterConfigException ex) {
assertExceptionCodeWithArgCount(ex, GeoServerOAuth2FilterConfigException.OAUTH2_JWK_SET_URI_REQUIRED, 0);
}
config.setOidcJwkSetUri("lalala");
try {
lValidate.call();
} catch (GeoServerOAuth2FilterConfigException ex) {
assertExceptionCodeWithArgCount(ex, GeoServerOAuth2FilterConfigException.OAUTH2_WKTS_URL_MALFORMED, 0);
}
config.setOidcJwkSetUri("https://jwkset");
validator.validateOAuth2FilterConfig(config);
config.setOidcUsePKCE(true);
config.setOidcClientSecret(null);
validator.validateOAuth2FilterConfig(config);
config.setOidcUsePKCE(false);
config.setOidcClientSecret("oauth2clientsecret");
config.setGoogleEnabled(true);
config.setGoogleClientId("googleclientId");
config.setGoogleClientSecret("googleClientSecret");
config.setGitHubEnabled(true);
config.setGitHubClientId("gitHubClientId");
config.setGitHubClientSecret("gitHubClientSecret");
config.setMsEnabled(true);
config.setMsClientId("msClientId");
config.setMsClientSecret("msClientSecret");
}
@Test
public void testRoleSourceIdToken() throws Exception {
GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
config.setClassName(GeoServerOAuth2LoginAuthenticationFilter.class.getName());
config.setName("testOAuth2");
// given: roleSource ID Token
config.setRoleSource(OpenIdRoleSource.IdToken);
// when: Google enabled
config.setGoogleEnabled(true);
config.setGoogleClientId("gid");
config.setGoogleClientSecret("gs");
// then: OK
validator.validateOAuth2FilterConfig(config);
// when: MS enabled
config.setMsEnabled(true);
config.setMsClientId("mid");
config.setMsClientSecret("ms");
// then: OK
validator.validateOAuth2FilterConfig(config);
// when: OIDC enabled
enableOidcValid(config);
// then: OK
validator.validateOAuth2FilterConfig(config);
// when: github enabled
config.setGitHubEnabled(true);
config.setGitHubClientId("ghid");
config.setGitHubClientSecret("ghs");
// then: fail, not supported
try {
validator.validateOAuth2FilterConfig(config);
fail("Expected FilterConfigException");
} catch (FilterConfigException ex) {
assertExceptionCodeWithArgCount(
ex, GeoServerOAuth2FilterConfigException.ROLE_SOURCE_ID_TOKEN_INVALID_FOR_GITHUB, 0);
}
}
@Test
public void testRoleSourceUserInfo() throws Exception {
GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
config.setClassName(GeoServerOAuth2LoginAuthenticationFilter.class.getName());
config.setName("testOAuth2");
// given: roleSource user Info
config.setRoleSource(OpenIdRoleSource.UserInfo);
// when: OIDC enabled
enableOidcValid(config);
// then: fail, not supported
try {
validator.validateOAuth2FilterConfig(config);
fail("Expected FilterConfigException");
} catch (FilterConfigException ex) {
assertExceptionCodeWithArgCount(
ex, GeoServerOAuth2FilterConfigException.ROLE_SOURCE_USER_INFO_URI_REQUIRED, 0);
}
config.setOidcUserInfoUri("https://userinfo");
validator.validateOAuth2FilterConfig(config);
}
private void enableOidcValid(GeoServerOAuth2LoginFilterConfig config) {
config.setOidcEnabled(true);
config.setOidcClientId("oid");
config.setOidcClientSecret("os");
config.setOidcAuthorizationUri("https://a");
config.setOidcTokenUri("https://t");
config.setOidcJwkSetUri("https://j");
}
@Test
public void testOAuth2FilterConfigValidationForMsGraph() throws Exception {
GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
config.setClassName(GeoServerOAuth2LoginAuthenticationFilter.class.getName());
config.setName("testOAuth2");
// given: role source MSGraph
config.setRoleSource(OpenIdRoleSource.MSGraphAPI);
// Google IDP
config.setGoogleEnabled(true);
config.setGoogleClientId("ci");
config.setGoogleClientSecret("cs");
try {
// when: validate
validator.validateOAuth2FilterConfig(config);
fail("Expected FilterConfigException");
} catch (FilterConfigException ex) {
assertExceptionCodeWithArgCount(ex, MSGRAPH_COMBINATION_INVALID, 0);
}
// given: additionally MS
config.setMsEnabled(true);
config.setMsClientId("ci");
config.setMsClientSecret("cs");
try {
// when: validate
validator.validateOAuth2FilterConfig(config);
fail("Expected FilterConfigException");
} catch (FilterConfigException ex) {
// then: still failed - combine not allowed
assertExceptionCodeWithArgCount(ex, MSGRAPH_COMBINATION_INVALID, 0);
}
// given: only MS
config.setGoogleEnabled(false);
// when: validate, then: OK
validator.validateOAuth2FilterConfig(config);
}
private void assertExceptionCodeWithArgCount(FilterConfigException pException, String pCode, int pExArgCount) {
assertEquals(pCode, pException.getId());
assertEquals(pExArgCount, pException.getArgs().length);
LOGGER.info(pException.getMessage());
}
}

View File

@ -0,0 +1,366 @@
/*
* (c) 2020 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.security.oauth2.login;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.apache.commons.io.IOUtils.resourceToString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.extension.ResponseTransformerV2;
import com.github.tomakehurst.wiremock.http.HttpHeader;
import com.github.tomakehurst.wiremock.http.HttpHeaders;
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.Response;
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.MACSigner;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.servlet.Filter;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestEvent;
import javax.servlet.http.HttpSession;
import org.geoserver.data.test.SystemTestData;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.security.GeoServerSecurityFilterChain;
import org.geoserver.security.GeoServerSecurityFilterChainProxy;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.HtmlLoginFilterChain;
import org.geoserver.security.RequestFilterChain;
import org.geoserver.security.VariableFilterChain;
import org.geoserver.security.config.SecurityManagerConfig;
import org.geoserver.test.GeoServerSystemTestSupport;
import org.geoserver.test.TestSetup;
import org.geoserver.test.TestSetupFrequency;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.RequestContextListener;
@TestSetup(run = TestSetupFrequency.REPEAT)
public class GeoServerOAuth2LoginIntegrationTest extends GeoServerSystemTestSupport {
/** Puts the client (=GeoServer) created random nonce into the id token which is embedded in the access token. */
private class TokenEndpointBasicTransformer implements ResponseTransformerV2 {
@Override
public String getName() {
return "token-endpoint";
}
@Override
public Response transform(Response response, ServeEvent serveEvent) {
Request lRequest = serveEvent.getRequest();
try {
return transformImpl(lRequest);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error in wiremock.", e);
throw new RuntimeException(e);
}
}
private Response transformImpl(Request pRequest) throws Exception {
Charset cs = StandardCharsets.UTF_8;
String idTokenTempl = resourceToString("/OpenIdConnectIntegrationTest/id-token-tmpl.json", cs);
String accessTokenTempl = resourceToString("/OpenIdConnectIntegrationTest/token-response-tmpl.json", cs);
if (reqParamNonce == null) {
throw new IllegalArgumentException("nonce must not be null");
}
String idToken = idTokenTempl.replace("${nonce}", reqParamNonce);
byte[] secretKey = CLIENT_SECRET.getBytes();
JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
Payload payload = new Payload(idToken);
JWSObject jwsObject = new JWSObject(header, payload);
JWSSigner signer = new MACSigner(secretKey);
jwsObject.sign(signer);
String jwt = jwsObject.serialize();
String accessToken = accessTokenTempl.replace("${id_token}", jwt);
return Response.response()
.body(accessToken)
.headers(new HttpHeaders(HttpHeader.httpHeader("Content-Type", "application/json")))
.build();
}
@Override
public boolean applyGlobally() {
return false;
}
}
private static final String CLIENT_ID = "kbyuFDidLLm280LIwVFiazOqjO3ty8KH";
private static final String CLIENT_SECRET = "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa";
private static final String CODE = "R-2CqM7H1agwc7Cx";
private WireMockServer openIdService;
private String authService;
private String baseRedirectUri = "http://localhost:8080/geoserver/";
private String reqParamNonce;
public void setupWireMock() throws Exception {
System.setProperty(GeoServerOAuth2LoginFilterConfig.OPENID_TEST_GS_PROXY_BASE, "http://localhost/geoserver");
openIdService =
new WireMockServer(wireMockConfig().dynamicPort().extensions(new TokenEndpointBasicTransformer()));
// uncomment the following to get wiremock logging
// .notifier(new ConsoleNotifier(true)));
openIdService.start();
openIdService.stubFor(WireMock.get(urlEqualTo(".well-known/jwks.json"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBodyFile("jwks.json")));
openIdService.stubFor(WireMock.get(WireMock.urlMatching(".*/userinfo")) // disallow query
// parameters
/*
* .withHeader( "Authorization", equalTo("Bearer CPURR33RUz-gGhjwODTd9zXo5JkQx4wS"))
*/
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBodyFile("userinfo.json")));
}
@Override
protected void onTearDown(SystemTestData pTestData) throws Exception {
if (openIdService != null) {
openIdService.shutdown();
}
super.onTearDown(pTestData);
}
@Override
protected void onSetUp(SystemTestData testData) throws Exception {
setupWireMock();
super.onSetUp(testData);
// prepare mock server base path
authService = "http://localhost:" + openIdService.port();
// setup openid
GeoServerSecurityManager manager = getSecurityManager();
GeoServerOAuth2LoginFilterConfig filterConfig = new GeoServerOAuth2LoginFilterConfig();
filterConfig.setName("openidconnect");
filterConfig.setClassName(GeoServerOAuth2LoginAuthenticationFilter.class.getName());
filterConfig.setOidcEnabled(true);
filterConfig.setOidcClientId(CLIENT_ID);
filterConfig.setOidcClientSecret(CLIENT_SECRET);
filterConfig.setBaseRedirectUri(baseRedirectUri);
filterConfig.calculateRedirectUris();
filterConfig.setOidcTokenUri(authService + "/token");
filterConfig.setOidcAuthorizationUri(authService + "/authorize");
filterConfig.setOidcUserInfoUri(authService + "/userinfo");
filterConfig.setOidcLogoutUri(authService + "/endSession");
filterConfig.setOidcJwkSetUri(authService + "/.well-known/jwks.json");
filterConfig.setOidcEnforceTokenValidation(false);
filterConfig.setOidcScopes("openid profile email phone address");
filterConfig.setEnableRedirectAuthenticationEntryPoint(true);
filterConfig.setOidcUserNameAttribute("email");
filterConfig.setRoleSource(GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource.IdToken);
filterConfig.setTokenRolesClaim("roles");
// for ease of testing, do not use HTTPS
filterConfig.setOidcForceAuthorizationUriHttps(false);
filterConfig.setOidcForceTokenUriHttps(false);
filterConfig.setOidcJwsAlgorithmName(JwsAlgorithms.HS256);
manager.saveFilter(filterConfig);
SecurityManagerConfig config = manager.getSecurityConfig();
GeoServerSecurityFilterChain chain = config.getFilterChain();
RequestFilterChain www = chain.getRequestChainByName("web");
www.setFilterNames("openidconnect", "anonymous");
HtmlLoginFilterChain lLoginAuthorizeChain =
new HtmlLoginFilterChain("/oauth2/authorization/**", "/login/oauth2/code/**");
lLoginAuthorizeChain.setAllowSessionCreation(true);
lLoginAuthorizeChain.setName("oauth2-endpoints");
lLoginAuthorizeChain.setFilterNames("openidconnect");
chain.getRequestChains().add(0, lLoginAuthorizeChain);
manager.saveSecurityConfig(config);
}
/** Enable the Spring Security authentication filters, we want the test to be complete and realistic */
@Override
protected List<Filter> getFilters() {
SecurityManagerConfig mconfig = getSecurityManager().getSecurityConfig();
GeoServerSecurityFilterChain filterChain = mconfig.getFilterChain();
VariableFilterChain chain = (VariableFilterChain) filterChain.getRequestChainByName("web");
List<Filter> result = new ArrayList<>();
for (String filterName : chain.getCompiledFilterNames()) {
try {
result.add(getSecurityManager().loadFilter(filterName));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return result;
}
@Test
public void testRoleExtraction() throws Exception {
// request token with basic auth
openIdService.stubFor(WireMock.post(urlPathEqualTo("/token"))
.withBasicAuth(CLIENT_ID, CLIENT_SECRET)
.withRequestBody(containing("grant_type=authorization_code"))
.withRequestBody(containing("code=" + CODE))
.withRequestBody(containing(
"redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fgeoserver%2Flogin%2Foauth2%2Fcode%2Foidc"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withTransformers("token-endpoint")));
verifyLoginLogout();
}
@Test
public void testClientConfidental() throws Exception {
GeoServerSecurityManager manager = getSecurityManager();
GeoServerOAuth2LoginFilterConfig config =
(GeoServerOAuth2LoginFilterConfig) manager.loadFilterConfig("openidconnect", true);
config.setOidcAuthenticationMethodPostSecret(true);
manager.saveFilter(config);
// request token with secret in post body
openIdService.stubFor(WireMock.post(urlPathEqualTo("/token"))
.withRequestBody(containing("grant_type=authorization_code"))
.withRequestBody(containing("client_id=" + CLIENT_ID))
.withRequestBody(containing("client_secret=" + CLIENT_SECRET))
.withRequestBody(containing("code=" + CODE))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withTransformers("token-endpoint")));
verifyLoginLogout();
}
private void verifyLoginLogout() throws IOException, ServletException {
// given: request a protected URL
MockHttpServletRequest webRequest = createRequest("web/");
// when: execute request
MockHttpServletResponse webResponse = executeOnSecurityFilters(webRequest);
HttpSession lSession = webRequest.getSession();
// then: "skip login dialog"/AEP is enabled -> spring's initiate login endpoint
assertEquals(302, webResponse.getStatus());
String location = webResponse.getHeader("Location");
String authStartPath = "oauth2/authorization/oidc";
assertEquals(baseRedirectUri + authStartPath, location);
// when: request to spring's initiate login endpoint is issued on same session
webRequest = createRequest("/" + authStartPath);
webRequest.setSession(lSession);
webResponse = executeOnSecurityFilters(webRequest);
// then: response is forward to auth server login
location = webResponse.getHeader("Location");
assertNotNull(location);
assertThat(location, CoreMatchers.startsWith(authService));
Map<String, Object> kvp = KvpUtils.parseQueryString(location);
assertThat(kvp, Matchers.hasEntry("client_id", CLIENT_ID));
assertThat(kvp, Matchers.hasEntry("redirect_uri", "http://localhost:8080/geoserver/login/oauth2/code/oidc"));
assertThat(kvp, Matchers.hasEntry("scope", "openid profile email phone address"));
assertThat(kvp, Matchers.hasEntry("response_type", "code"));
Object state = kvp.get("state");
assertNotNull(state);
Object lNonce = kvp.get("nonce");
assertNotNull(lNonce);
reqParamNonce = lNonce.toString();
// make believe we authenticated and got the redirect back, with the code
MockHttpServletRequest codeRequest = createRequest("login/oauth2/code/oidc?code=" + CODE + "&state=" + state);
codeRequest.setSession(lSession);
executeOnSecurityFilters(codeRequest);
// should have authenticated and given roles, and they have been saved in the session
SecurityContext context = new HttpSessionSecurityContextRepository()
.loadDeferredContext(codeRequest)
.get();
Authentication auth = context.getAuthentication();
assertNotNull(auth);
assertEquals(DefaultOidcUser.class, auth.getPrincipal().getClass());
DefaultOidcUser lUser = (DefaultOidcUser) auth.getPrincipal();
assertEquals("andrea.aime@gmail.com", lUser.getName());
assertEquals(lNonce, lUser.getNonce());
assertThat(
auth.getAuthorities().stream().map(a -> a.getAuthority()).collect(Collectors.toList()),
CoreMatchers.hasItems("R1", "R2", "ROLE_AUTHENTICATED"));
// given: id token
String lIdTokenValue = lUser.getIdToken().getTokenValue();
assertNotNull(lIdTokenValue);
// when: logout
webRequest = createRequest("/logout");
webRequest.setSession(lSession);
webResponse = executeOnSecurityFilters(webRequest);
// then: id token value must be in id_token_hint
location = webResponse.getHeader("Location");
assertNotNull(location);
kvp = KvpUtils.parseQueryString(location);
assertThat(kvp, Matchers.hasEntry("id_token_hint", lIdTokenValue));
}
private MockHttpServletResponse executeOnSecurityFilters(MockHttpServletRequest request)
throws IOException, javax.servlet.ServletException {
// for session local support in Spring
new RequestContextListener().requestInitialized(new ServletRequestEvent(request.getServletContext(), request));
// run on the
MockFilterChain chain = new MockFilterChain();
MockHttpServletResponse response = new MockHttpServletResponse();
GeoServerSecurityFilterChainProxy filterChainProxy =
GeoServerExtensions.bean(GeoServerSecurityFilterChainProxy.class);
filterChainProxy.doFilter(request, response, chain);
return response;
}
@After
public void clear() {
SecurityContextHolder.clearContext();
RequestContextHolder.resetRequestAttributes();
}
}

View File

@ -0,0 +1,23 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.login;
import static org.junit.Assert.assertArrayEquals;
import org.junit.Test;
/** Tests for {@link org.geoserver.security.oauth2.login.ScopeUtils}. */
public class ScopeUtilsTest {
@Test
public void testValueOf() {
assertArrayEquals(new String[] {"a"}, ScopeUtils.valueOf("a"));
assertArrayEquals(new String[] {"a"}, ScopeUtils.valueOf(" a "));
assertArrayEquals(new String[] {"a", "b"}, ScopeUtils.valueOf("a b"));
assertArrayEquals(new String[] {"a", "b"}, ScopeUtils.valueOf(" a, b "));
assertArrayEquals(new String[] {}, ScopeUtils.valueOf(" "));
assertArrayEquals(new String[] {}, ScopeUtils.valueOf(null));
}
}

View File

@ -0,0 +1,172 @@
/*
* (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.security.oauth2.spring;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_GOOGLE;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_OIDC;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.REGISTRATION_ID;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
/**
* Tests for {@link GeoServerAuthorizationRequestCustomizer}.
*
* @author awaterme
*/
@SuppressWarnings("unchecked")
public class GeoServerAuthorizationRequestCustomizerTest {
private Builder mockBuilder = Mockito.mock(Builder.class);
private GeoServerOAuth2LoginFilterConfig config;
private Map<String, Object> attributes = new HashMap<>();
private Map<String, Object> additionalParams = new HashMap<>();
private GeoServerAuthorizationRequestCustomizer sut;
@Before
public void setUpConfig() {
System.setProperty(GeoServerOAuth2LoginFilterConfig.OPENID_TEST_GS_PROXY_BASE, "http://localhost/geoserver");
config = new GeoServerOAuth2LoginFilterConfig();
sut = new GeoServerAuthorizationRequestCustomizer(config);
}
@Before
public void setUpMocks() {
// when GeoServerAuthorizationRequestCustomizer calls attributes(pCustomizer)
when(mockBuilder.attributes(any(Consumer.class)))
.then(
// then call the customizer with our "attributes"
stub -> {
Consumer<Map<String, Object>> l = stub.getArgument(0, Consumer.class);
l.accept(attributes);
return mockBuilder;
});
// when GeoServerAuthorizationRequestCustomizer calls additionalParameters(pCustomizer)
when(mockBuilder.additionalParameters(any(Consumer.class)))
.then(
// then call the customizer with our "additionalParameters"
stub -> {
Consumer<Map<String, Object>> l = stub.getArgument(0, Consumer.class);
l.accept(additionalParams);
return mockBuilder;
});
}
/** verifies no PKCE parameters are present when not enabled */
@Test
public void testNoPkceWithOidc() {
// given: request is for OIDC provider
attributes.put(REGISTRATION_ID, REG_ID_OIDC);
// when: customizer is invoker, without PKCE enabled
sut.accept(mockBuilder);
// then: no PKCE parameters
assertTrue(additionalParams.isEmpty());
}
/** verifies PKCE parameters are present when enabled for OIDC */
@Test
public void testPkceWithOidc() {
// given: request is for OIDC provider
attributes.put(REGISTRATION_ID, REG_ID_OIDC);
// when: customizer is invoker, with PKCE enabled
config.setOidcUsePKCE(true);
sut.accept(mockBuilder);
// then: PKCE parameters
assertTrue(additionalParams.containsKey(PkceParameterNames.CODE_CHALLENGE));
assertTrue(additionalParams.containsKey(PkceParameterNames.CODE_CHALLENGE_METHOD));
}
/** verifiesPKCE parameters are present for Google */
@Test
public void testPkceWithGoogle() {
testPkceForRegistrationId(REG_ID_GOOGLE);
}
/** verifiesPKCE parameters are present for GitHub */
@Test
public void testPkceWithGitHub() {
testPkceForRegistrationId(GeoServerOAuth2ClientRegistrationId.REG_ID_GIT_HUB);
}
/** verifiesPKCE parameters are present for Microsoft */
@Test
public void testPkceWithMs() {
testPkceForRegistrationId(GeoServerOAuth2ClientRegistrationId.REG_ID_MICROSOFT);
}
private void testPkceForRegistrationId(String pRegistrationId) {
// given: request is for OIDC provider
attributes.put(REGISTRATION_ID, pRegistrationId);
// when: customizer is invoker, with PKCE enabled
config.setOidcUsePKCE(false);
sut.accept(mockBuilder);
// then: PKCE parameters
assertTrue(additionalParams.containsKey(PkceParameterNames.CODE_CHALLENGE));
assertTrue(additionalParams.containsKey(PkceParameterNames.CODE_CHALLENGE_METHOD));
}
/** verifies response_mode is passed as extra parameter if activated */
@Test
public void testResponseModeQueryOIDC() {
// given: request is for OIDC provider
attributes.put(REGISTRATION_ID, REG_ID_OIDC);
// when: customizer is invoker, with responseMode=query
config.setOidcResponseMode("query");
sut.accept(mockBuilder);
// then:
assertEquals("query", additionalParams.get("response_mode"));
}
/** verifies no response_mode extra parameter if not activated */
@Test
public void testNoResponseModeOIDC() {
// given: request is for OIDC provider
attributes.put(REGISTRATION_ID, REG_ID_OIDC);
// when: customizer is invoker, with responseMode=query
config.setOidcResponseMode(null);
sut.accept(mockBuilder);
// then:
assertNull(additionalParams.get("response_mode"));
}
/** verifies no response_mode extra parameter for Google */
@Test
public void testNoResponseModeGoogle() {
// given: request is for OIDC provider
attributes.put(REGISTRATION_ID, REG_ID_GOOGLE);
// when: customizer is invoker, with responseMode=query
config.setOidcResponseMode("query");
sut.accept(mockBuilder);
// then:
assertNull(additionalParams.get("response_mode"));
}
}

View File

@ -0,0 +1,87 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.oauth2.spring;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2ClientRegistrationId.REG_ID_OIDC;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.logging.Level;
import org.geoserver.security.oauth2.common.ConfidentialLogger;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
/** Tests for {@link GeoServerOAuth2AccessTokenResponseClient} */
public class GeoServerOAuth2AccessTokenResponseClientTest {
@SuppressWarnings("unchecked")
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> mockDelegate =
mock(OAuth2AccessTokenResponseClient.class);
private ClientRegistration mockClientRegistration = mock(ClientRegistration.class);
private GeoServerOidcIdTokenDecoderFactory jwtDecoderFactory = new GeoServerOidcIdTokenDecoderFactory();
private GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
private GeoServerOAuth2AccessTokenResponseClient sut =
new GeoServerOAuth2AccessTokenResponseClient(mockDelegate, jwtDecoderFactory);
private Level originalLevel;
@Before
public void setUp() {
originalLevel = ConfidentialLogger.getLevel();
ConfidentialLogger.setLevel(Level.FINE);
ConfidentialLogger.setEnabled(true);
when(mockClientRegistration.getRegistrationId()).thenReturn(REG_ID_OIDC);
jwtDecoderFactory.setGeoServerOAuth2LoginFilterConfig(config);
}
@After
public void tearDown() {
ConfidentialLogger.setLevel(originalLevel);
}
@Test
public void testSmoke() {
// given
var authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("myAuthorizationUri")
.redirectUri("myRedirectUri")
.clientId("myClientId")
.build();
var authorizationResponse = OAuth2AuthorizationResponse.success("code")
.redirectUri("myRedirectUri")
.build();
var lExchange = new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
var lRequest = new OAuth2AuthorizationCodeGrantRequest(mockClientRegistration, lExchange);
var lDelegatesResponse = OAuth2AccessTokenResponse.withToken("myToken")
.tokenType(TokenType.BEARER)
.build();
// when
when(mockDelegate.getTokenResponse(any())).thenReturn(lDelegatesResponse);
OAuth2AccessTokenResponse lTokenResponse = sut.getTokenResponse(lRequest);
// then
assertNotNull(lTokenResponse);
assertSame(lDelegatesResponse, lTokenResponse);
}
}

View File

@ -0,0 +1,26 @@
{
"clientID": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
"created_at": "2020-12-01T16:10:14.659Z",
"email": "andrea.aime@gmail.com",
"email_verified": true,
"family_name": "Aime",
"given_name": "Andrea",
"roles": [
"R1",
"R2"
],
"locale": "it",
"name": "Andrea Aime",
"nickname": "andrea.aime",
"updated_at": "2020-12-14T16:27:04.884Z",
"user_id": "100301874944276879963462152",
"persistent": {},
"user_metadata": {},
"app_metadata": {},
"iss": "https://samples.auth0.com/",
"sub": "100301874944276879963462152",
"aud": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
"iat": 1607963235,
"exp": 1607999235,
"nonce": "${nonce}"
}

View File

@ -0,0 +1,7 @@
{
"access_token": "CPURR33RUz-gGhjwODTd9zXo5JkQx4wS",
"id_token": "${id_token}",
"scope": "openid profile email phone address",
"expires_in": 86400,
"token_type": "Bearer"
}

View File

@ -0,0 +1,40 @@
{
keys: [
{
alg: "RS256",
kty: "RSA",
use: "sig",
n: "pB-AhRkieLN5sAgc2hhsMWvScc329YmuJ1LpsW7LmgezwpWWYKzUIjkdzF1TVfVuhdQ_sI0-qBRzqO0zpFSNtiP33912UxNBd-VFBxlkbYkOC3WccDj03ndi2sdxdgxMpd2NAoLlCm6trEoIbx2HIIDOmo9zed1QbJwYf5Ha1EQy8dUWKgSC-hb5IW_1f7_7vVCoWTNAg0EXn_RWe0fKvYnvXJ2wzo9XU_XeuJIiSGLU62htIDq7OCyPuCitBGbuUe1KNOdyCu5HzWrFoQ5JfMsTWJA8cH3CLgHA5i4C5wCOLX1uW3ibsPv8O-TzvxMM8LJ76aV2gM-3t1n_INclhQ",
e: "AQAB",
kid: "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg",
x5t: "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg",
x5c: [
"MIIDCzCCAfOgAwIBAgIJAJP6qydiMpsuMA0GCSqGSIb3DQEBBQUAMBwxGjAYBgNVBAMMEXNhbXBsZXMuYXV0aDAuY29tMB4XDTE0MDUyNjIyMDA1MFoXDTI4MDIwMjIyMDA1MFowHDEaMBgGA1UEAwwRc2FtcGxlcy5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkH4CFGSJ4s3mwCBzaGGwxa9Jxzfb1ia4nUumxbsuaB7PClZZgrNQiOR3MXVNV9W6F1D+wjT6oFHOo7TOkVI22I/ff3XZTE0F35UUHGWRtiQ4LdZxwOPTed2Lax3F2DEyl3Y0CguUKbq2sSghvHYcggM6aj3N53VBsnBh/kdrURDLx1RYqBIL6Fvkhb/V/v/u9UKhZM0CDQRef9FZ7R8q9ie9cnbDOj1dT9d64kiJIYtTraG0gOrs4LI+4KK0EZu5R7Uo053IK7kfNasWhDkl8yxNYkDxwfcIuAcDmLgLnAI4tfW5beJuw+/w75PO/EwzwsnvppXaAz7e3Wf8g1yWFAgMBAAGjUDBOMB0GA1UdDgQWBBTsmytFLNox+NUZdTNlCUL3hHrngTAfBgNVHSMEGDAWgBTsmytFLNox+NUZdTNlCUL3hHrngTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAodbRX/34LnWB70l8dpDF1neDoG29F0XdpE9ICWHeWB1gb/FvJ5UMy9/pnL0DI3mPwkTDDob+16Zc68o6dT6sH3vEUP1iRreJlFADEmJZjrH9P4Y7ttx3G2Uw2RU5uucXIqiyMDBrQo4vx4Lnghl+b/WYbZJgzLfZLgkOEjcznS0Yi5Wdz6MvaL3FehSfweHyrjmxz0e8elHq7VY8OqRA+4PmUBce9BgDCk9fZFjgj8l0m9Vc5pPKSY9LMmTyrYkeDr/KppqdXKOCHmv7AIGb6rMCtbkIL/CM7Bh9Hx78/UKAz87Sl9A1yXVNjKbZwOEW60ORIwJmd8Tv46gJF+/rV"
]
},
{
alg: "RS256",
kty: "RSA",
use: "sig",
n: "ruHd18KERyQNwrdtdH4P3trxUVqvpGmt9IOQeTd4SRQVhJ4ziyFrJ2iI4oNB_RbbhEnzUAvx53Z-45wZ5T5XWdAkJA-XoGroiLrxq40XMzHCQaEDPAsVLtMsYjfkcw9sIdhPBsfrvvq7zfx4hYxpSTAj_iJczKTpN9BdPGLdBY70-OIUcel_FohD4v3apI_sJIcS9OcaTF7FZbdgTXyQt4M8mIcsA_f7iswO-UnG0CV19Q5WYUYkCi1aE7LzjU81rwRxG18Wfk5PlucyQ9g6Pf0MSfPdydxPLs1lGF5t6uW1TGBlHChyFACb5YJiCx42oohBH_czGHVMlh-E5laitQ",
e: "AQAB",
kid: "UW65QUcG827BofOEU7QXY",
x5t: "EREKV8RoqxE1HA9ilSLcfDJNybo",
x5c: [
"MIIC/TCCAeWgAwIBAgIJdO8ZIceNeC9aMA0GCSqGSIb3DQEBCwUAMBwxGjAYBgNVBAMTEXNhbXBsZXMuYXV0aDAuY29tMB4XDTIwMDMxODEyNDQxOVoXDTMzMTEyNTEyNDQxOVowHDEaMBgGA1UEAxMRc2FtcGxlcy5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu4d3XwoRHJA3Ct210fg/e2vFRWq+kaa30g5B5N3hJFBWEnjOLIWsnaIjig0H9FtuESfNQC/Hndn7jnBnlPldZ0CQkD5egauiIuvGrjRczMcJBoQM8CxUu0yxiN+RzD2wh2E8Gx+u++rvN/HiFjGlJMCP+IlzMpOk30F08Yt0FjvT44hRx6X8WiEPi/dqkj+wkhxL05xpMXsVlt2BNfJC3gzyYhywD9/uKzA75ScbQJXX1DlZhRiQKLVoTsvONTzWvBHEbXxZ+Tk+W5zJD2Do9/QxJ893J3E8uzWUYXm3q5bVMYGUcKHIUAJvlgmILHjaiiEEf9zMYdUyWH4TmVqK1AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKr6X/Ipphqb9TtHWzbSFWXSj4VoMA4GA1UdDwEB/wQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAWTSl9jutM2LO7iKYd5gfnFn5Ke5wVLL/tpvjgQ2GCCwC6ya2JAyvWvypVYGkbyGw50shpcJNwEgkR6I5USqFrj4gyonlgBULNJ6i91kL3Pav5j6UE8iXFyfQLbwxH57tXl5lXNtdmXm0KcvNUFb/9JoH22HHa62uLfWN9i9Y2YH8ycpQP6C8BrB65TA2zXU5yHLZZ4J+upnZDvrET9eZa+NGE9e+scf+YhiqKkbxnz/TIjDWKsWjRP40seQqKVZWnxw1Rm3Rp6Y+43j5e3jJkR68x+Ct2Cuc3N3ffeJNqstq/5JNSWTmJ0DJKftX7/Npecumi0Co5RJcet0n+QsXsg=="
]
},
{
alg: "RS256",
kty: "RSA",
use: "sig",
n: "ruHd18KERyQNwrdtdH4P3trxUVqvpGmt9IOQeTd4SRQVhJ4ziyFrJ2iI4oNB_RbbhEnzUAvx53Z-45wZ5T5XWdAkJA-XoGroiLrxq40XMzHCQaEDPAsVLtMsYjfkcw9sIdhPBsfrvvq7zfx4hYxpSTAj_iJczKTpN9BdPGLdBY70-OIUcel_FohD4v3apI_sJIcS9OcaTF7FZbdgTXyQt4M8mIcsA_f7iswO-UnG0CV19Q5WYUYkCi1aE7LzjU81rwRxG18Wfk5PlucyQ9g6Pf0MSfPdydxPLs1lGF5t6uW1TGBlHChyFACb5YJiCx42oohBH_czGHVMlh-E5laitQ",
"e": "XXXX",
"kid":null,
x5t: "EREKV8RoqxE1HA9ilSLcfDJNybo",
x5c: [
"MIIC/TCCAeWgAwIBAgIJdO8ZIceNeC9aMA0GCSqGSIb3DQEBCwUAMBwxGjAYBgNVBAMTEXNhbXBsZXMuYXV0aDAuY29tMB4XDTIwMDMxODEyNDQxOVoXDTMzMTEyNTEyNDQxOVowHDEaMBgGA1UEAxMRc2FtcGxlcy5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu4d3XwoRHJA3Ct210fg/e2vFRWq+kaa30g5B5N3hJFBWEnjOLIWsnaIjig0H9FtuESfNQC/Hndn7jnBnlPldZ0CQkD5egauiIuvGrjRczMcJBoQM8CxUu0yxiN+RzD2wh2E8Gx+u++rvN/HiFjGlJMCP+IlzMpOk30F08Yt0FjvT44hRx6X8WiEPi/dqkj+wkhxL05xpMXsVlt2BNfJC3gzyYhywD9/uKzA75ScbQJXX1DlZhRiQKLVoTsvONTzWvBHEbXxZ+Tk+W5zJD2Do9/QxJ893J3E8uzWUYXm3q5bVMYGUcKHIUAJvlgmILHjaiiEEf9zMYdUyWH4TmVqK1AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKr6X/Ipphqb9TtHWzbSFWXSj4VoMA4GA1UdDwEB/wQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAWTSl9jutM2LO7iKYd5gfnFn5Ke5wVLL/tpvjgQ2GCCwC6ya2JAyvWvypVYGkbyGw50shpcJNwEgkR6I5USqFrj4gyonlgBULNJ6i91kL3Pav5j6UE8iXFyfQLbwxH57tXl5lXNtdmXm0KcvNUFb/9JoH22HHa62uLfWN9i9Y2YH8ycpQP6C8BrB65TA2zXU5yHLZZ4J+upnZDvrET9eZa+NGE9e+scf+YhiqKkbxnz/TIjDWKsWjRP40seQqKVZWnxw1Rm3Rp6Y+43j5e3jJkR68x+Ct2Cuc3N3ffeJNqstq/5JNSWTmJ0DJKftX7/Npecumi0Co5RJcet0n+QsXsg=="
]
}
]
}

View File

@ -0,0 +1,5 @@
These resources have been copied and adapted from the demo server used by the
OpenID Connect Playground:
* https://openidconnect.net/
* https://samples.auth0.com/

View File

@ -0,0 +1,25 @@
{
"clientID": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
"created_at": "2020-12-01T16:10:14.659Z",
"email": "andrea.aime@gmail.com",
"email_verified": true,
"family_name": "Aime",
"given_name": "Andrea",
"roles": [
"R1",
"R2"
],
"locale": "it",
"name": "Andrea Aime",
"nickname": "andrea.aime",
"updated_at": "2020-12-14T16:27:04.884Z",
"user_id": "100301874944276879963462152",
"persistent": {},
"user_metadata": {},
"app_metadata": {},
"iss": "https://samples.auth0.com/",
"sub": "100301874944276879963462152",
"aud": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
"iat": 1607963235,
"exp": 1607999235
}

View File

@ -0,0 +1,106 @@
{
"issuer": "https://server.example.com",
"authorization_endpoint": "https://server.example.com/connect/authorize",
"token_endpoint": "https://server.example.com/connect/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"private_key_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256",
"ES256"
],
"userinfo_endpoint": "https://server.example.com/connect/userinfo",
"check_session_iframe": "https://server.example.com/connect/check_session",
"end_session_endpoint": "https://server.example.com/connect/end_session",
"jwks_uri": "https://server.example.com/jwks.json",
"registration_endpoint": "https://server.example.com/connect/register",
"scopes_supported": [
"openid",
"profile",
"email",
"address",
"phone",
"offline_access"
],
"response_types_supported": [
"code",
"code id_token",
"id_token",
"token id_token"
],
"acr_values_supported": [
"urn:mace:incommon:iap:silver",
"urn:mace:incommon:iap:bronze"
],
"subject_types_supported": [
"public",
"pairwise"
],
"userinfo_signing_alg_values_supported": [
"RS256",
"ES256",
"HS256"
],
"userinfo_encryption_alg_values_supported": [
"RSA1_5",
"A128KW"
],
"userinfo_encryption_enc_values_supported": [
"A128CBC-HS256",
"A128GCM"
],
"id_token_signing_alg_values_supported": [
"RS256",
"ES256",
"HS256"
],
"id_token_encryption_alg_values_supported": [
"RSA1_5",
"A128KW"
],
"id_token_encryption_enc_values_supported": [
"A128CBC-HS256",
"A128GCM"
],
"request_object_signing_alg_values_supported": [
"none",
"RS256",
"ES256"
],
"display_values_supported": [
"page",
"popup"
],
"claim_types_supported": [
"normal",
"distributed"
],
"claims_supported": [
"sub",
"iss",
"auth_time",
"acr",
"name",
"given_name",
"family_name",
"nickname",
"profile",
"picture",
"website",
"email",
"email_verified",
"locale",
"zoneinfo",
"http://example.info/claims/groups"
],
"claims_parameter_supported": true,
"service_documentation": "http://server.example.com/connect/service_documentation.html",
"ui_locales_supported": [
"en-US",
"en-GB",
"en-CA",
"fr-FR",
"fr-CA"
]
}

View File

@ -0,0 +1,30 @@
{
"keys": [
{
"kid": "_GWQ_xqJpff0orQuwADmIYoWwC8w8J_uuknjIYvQQL4",
"kty": "RSA",
"alg": "RSA-OAEP",
"use": "enc",
"n": "i56e8orKBWj7WDrOb-2bF-32jQP4G1FIfwGs5EJunbThm7XxTo9yiUkPWkDNTrkpKbYkZd_0eAgZ_DJIjZ622mLyNjkIpClnhQDV0cxKvheJf4wn1hdYCgNQHeuq16XNrvcSVN4OKDTPAJsNMmvggYh-PaTF8qQp-EwKt8kBwDWuq4iZ1f8a2g8G677oT4gianwjI-VejJaL7ArQ6GRBt2GjTSxI_ury1dyKtUwOCvdftuX5IRnYWs0Z6mbRoDtDKVf-xgTFOmgXMa3OWPJXEOPaaOuKENPFP39ADfZll41fGTA5nVlFdYvcFPUvGIM7195PkYS-ijdCuxeNxE5TYw",
"e": "AQAB",
"x5c": [
"MIICozCCAYsCBgGBRPidqzANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDApkYXZlLXJlYWxtMB4XDTIyMDYwODIwMTczMloXDTMyMDYwODIwMTkxMlowFTETMBEGA1UEAwwKZGF2ZS1yZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIuenvKKygVo+1g6zm/tmxft9o0D+BtRSH8BrORCbp204Zu18U6PcolJD1pAzU65KSm2JGXf9HgIGfwySI2ettpi8jY5CKQpZ4UA1dHMSr4XiX+MJ9YXWAoDUB3rqtelza73ElTeDig0zwCbDTJr4IGIfj2kxfKkKfhMCrfJAcA1rquImdX/GtoPBuu+6E+IImp8IyPlXoyWi+wK0OhkQbdho00sSP7q8tXcirVMDgr3X7bl+SEZ2FrNGepm0aA7QylX/sYExTpoFzGtzljyVxDj2mjrihDTxT9/QA32ZZeNXxkwOZ1ZRXWL3BT1LxiDO9feT5GEvoo3QrsXjcROU2MCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQTOL2VBr7UGAqwPrYrEXBBwbbIDG2048A1lmnvs0MA+m9MDm8L1lRm2Gg3pIfD+6/FqAmTbW6TJrhK1cTTMFEoJwM6p9IoCFamClIAJ3HVXrsHDgc2FRN8xdgOKKMOPrLgb2iwgX9tO35x6r4nGxUGsca15N7oyFklO122nYFoH0H00u24UDoQmeJ6CqX92e0HjPovQlTYWIeCG0QSh74HvOhfU1U4KB7hH9MNATG9mW6fP5jsm2g12CU1xNmcYVXEeQgFOG/AdcF97uKd7tojXrYKO7elBRGJf381R88s0UMW+d2YrxIWV9Isd82y3JHcCDDdhgTYNC7NP8Z+8BdA=="
],
"x5t": "kgLGxtKUaZo_fnrEC3cLu6GJq24",
"x5t#S256": "SHuakWsJp51jy6Iby88vS_6T5bOGcioX5pMNlm_bwYQ"
},
{
"kid": "U_U7yu1N8uJOWEX4QhT3qazCcNnGNWXS3kfRJ-Tdy8M",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "pdZfJzYixFM2378U-Tdwm_sLljO1tcSmxRKTZ7utmwBf7zYNMHCA41qsXhyjDdYQzXkkFMvW7gt66Wu-FCyjcThNmUXnoMYaaaC6vQM5xcgZriL6mkDAO1n5LD1chE6uVMOYKuP29LiYIWFy3xOIwPUzqewDCH-9W0IM_tLd-aX6rTidPqVMzKZxLsOVV0kcTQudv0DUiQ0R_6xnovvBdaAgoNm-2QjCBfMBXMEaESQPyRy65cXyQ7DCFSLSbpzJuBSJCVJI7gbuHgwq1pkiFo-dlmwssw9V3_8JdOhqZATK1yjjfyWgm56YtzbPrt5Mz4W1xTygfkMMpOr_SjFXxQ",
"e": "AQAB",
"x5c": [
"MIICozCCAYsCBgGBRPic+TANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDApkYXZlLXJlYWxtMB4XDTIyMDYwODIwMTczMloXDTMyMDYwODIwMTkxMlowFTETMBEGA1UEAwwKZGF2ZS1yZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKXWXyc2IsRTNt+/FPk3cJv7C5YztbXEpsUSk2e7rZsAX+82DTBwgONarF4cow3WEM15JBTL1u4LeulrvhQso3E4TZlF56DGGmmgur0DOcXIGa4i+ppAwDtZ+Sw9XIROrlTDmCrj9vS4mCFhct8TiMD1M6nsAwh/vVtCDP7S3fml+q04nT6lTMymcS7DlVdJHE0Lnb9A1IkNEf+sZ6L7wXWgIKDZvtkIwgXzAVzBGhEkD8kcuuXF8kOwwhUi0m6cybgUiQlSSO4G7h4MKtaZIhaPnZZsLLMPVd//CXToamQEytco438loJuemLc2z67eTM+FtcU8oH5DDKTq/0oxV8UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPyFcqmHhfbOPkjCE+JRfPY0Jdr3/O7FxhBpEqex+Z/T5lb8/X+r75Fu82rN6FKemwACQASq4UVEdDIMnlNjmXZLGA2CHRPPkV0zxpgyjK+Xt2F9PxE1FJQ0ljkjtA3rqn6WsAm36GAdp0mAsiRzB/5ovSj4NpaR+jyqiNV01HXjyy7an/cnU67ZkmJ6LdtMcsmMVmFMt5tfavoRGotBoAOrfaRRwZi3eltNXkmz7ctomLr3MY/GZb9Bp+tRb8APA+qupsQCsBZHkVe7Sli7tu6Z1Cj/OR9/htZxxvoPWDbLt88LMdwPzmxqgmhB0lKz42GpbzcD89QcSpwXKmudIMQ=="
],
"x5t": "9yuNMqnVX25rUd3akTytAn2NxBo",
"x5t#S256": "rGq0EtpTPHMdE_3GfysIQ0yKU2bZ4KINeksit0fxO9Y"
}
]
}

View File

@ -0,0 +1 @@
pdZfJzYixFM2378U-Tdwm_sLljO1tcSmxRKTZ7utmwBf7zYNMHCA41qsXhyjDdYQzXkkFMvW7gt66Wu-FCyjcThNmUXnoMYaaaC6vQM5xcgZriL6mkDAO1n5LD1chE6uVMOYKuP29LiYIWFy3xOIwPUzqewDCH-9W0IM_tLd-aX6rTidPqVMzKZxLsOVV0kcTQudv0DUiQ0R_6xnovvBdaAgoNm-2QjCBfMBXMEaESQPyRy65cXyQ7DCFSLSbpzJuBSJCVJI7gbuHgwq1pkiFo-dlmwssw9V3_8JdOhqZATK1yjjfyWgm56YtzbPrt5Mz4W1xTygfkMMpOr_SjFXxQ

View File

@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVX1U3eXUxTjh1Sk9XRVg0UWhUM3FhekNjTm5HTldYUzNrZlJKLVRkeThNIn0.eyJleHAiOjE3MDA2NzYyODcsImlhdCI6MTcwMDYxNjI4NywiYXV0aF90aW1lIjoxNzAwNjE1MzM3LCJqdGkiOiI1ZGI2OWViNy0yMGFkLTQ5M2ItYjE2YS1mNjEyYzRiNjA5N2MiLCJpc3MiOiJodHRwczovL2xvZ2luLWxpdmUtZGV2Lmdlb2NhdC5saXZlL3JlYWxtcy9kYXZlLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU3NDJhOGY2LTM5NzItNGQ2YS05OGYzLTFjZmU2MDc1ZjE0ZSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImxpdmUta2V5Iiwic2Vzc2lvbl9zdGF0ZSI6IjJkOWNlMDBjLWQ4ZTAtNDk0Ny1iODc4LTk2ZDlhZDYwYWU0OCIsImFjciI6IjAiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJkZWZhdWx0LXJvbGVzLWRhdmUtcmVhbG0iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImxpdmUta2V5Ijp7InJvbGVzIjpbIkdlb25ldHdvcmtBZG1pbmlzdHJhdG9yIiwiR2Vvc2VydmVyQWRtaW5pc3RyYXRvciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSBwaG9uZSBvZmZsaW5lX2FjY2VzcyBtaWNyb3Byb2ZpbGUtand0IGFkZHJlc3MiLCJzaWQiOiIyZDljZTAwYy1kOGUwLTQ5NDctYjg3OC05NmQ5YWQ2MGFlNDgiLCJ1cG4iOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYWRkcmVzcyI6e30sIm5hbWUiOiJkYXZpZCBibGFzYnkiLCJncm91cHMiOlsib2ZmbGluZV9hY2Nlc3MiLCJkZWZhdWx0LXJvbGVzLWRhdmUtcmVhbG0iLCJ1bWFfYXV0aG9yaXphdGlvbiJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCIsImdpdmVuX25hbWUiOiJkYXZpZCIsImZhbWlseV9uYW1lIjoiYmxhc2J5IiwiZW1haWwiOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCJ9.G7Ku40V9B1YcesTFbf9BeTkHlfpnSOmADByQ7AHMamV49K2yFzbRECSsTFH1Ijv_6lofWMnRWM8LUCQQH3HA2V6Y9WiYdfAiV52K9WQr38MDNIZXmpopCVZ9ML21EQnhojsbrDW5JkSQPtwnvwMW7OxFECQo8L4_eU8w6ShWVNwEP0JGPHPI3XCMOn-5Cicj6CHacMvhh1iaufGMOd7Dm8IMNZCtlanqoGLy3N3272n8SuydwN6uL0oD8pauYryY2VfCmSbciLNKy-B7NCkdxtF99vzV7J4y3yAac87V3tZmbO47x-X4DClhxlJ-Pr3p3R3VFW6xhCTd7UgOITVacQ

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ (c) 2018 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.geoserver.community.security</groupId>
<artifactId>gs-sec-oidc</artifactId>
<version>2.28-SNAPSHOT</version>
</parent>
<artifactId>gs-sec-oidc-web</artifactId>
<packaging>jar</packaging>
<name>GeoServer OpenID Connect Security Module - Web</name>
<dependencies>
<dependency>
<groupId>org.geoserver.web</groupId>
<artifactId>gs-web-sec-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.geoserver.community.security</groupId>
<artifactId>gs-sec-oidc-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-main</artifactId>
</dependency>
<dependency>
<groupId>org.geoserver.web</groupId>
<artifactId>gs-web-core</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver.security</groupId>
<artifactId>gs-security-tests</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver.web</groupId>
<artifactId>gs-web-sec-core</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.11.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,68 @@
/* (c) 2020 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.login;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geotools.util.SuppressFBWarnings;
import org.springframework.web.client.RestTemplate;
/** Client for auto-configuration of */
public class DiscoveryClient {
private static final String PROVIDER_END_PATH = "/.well-known/openid-configuration";
private static final String AUTHORIZATION_ENDPOINT_ATTR_NAME = "authorization_endpoint";
private static final String TOKEN_ENDPOINT_ATTR_NAME = "token_endpoint";
private static final String USERINFO_ENDPOINT_ATTR_NAME = "userinfo_endpoint";
private static final String END_SESSION_ENDPONT = "end_session_endpoint";
private static final String JWK_SET_URI_ATTR_NAME = "jwks_uri";
private static final String SCOPES_SUPPORTED = "scopes_supported";
private final RestTemplate restTemplate;
private String location;
public DiscoveryClient(String location) {
setLocation(location);
this.restTemplate = new RestTemplate();
}
public DiscoveryClient(String location, RestTemplate restTemplate) {
setLocation(location);
this.restTemplate = restTemplate;
}
private void setLocation(String location) {
if (!location.endsWith(PROVIDER_END_PATH)) {
location = ResponseUtils.appendPath(location, PROVIDER_END_PATH);
}
this.location = location;
}
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
public void autofill(GeoServerOAuth2LoginFilterConfig conf) {
Map response = restTemplate.getForObject(this.location, Map.class);
Optional.ofNullable(response.get(AUTHORIZATION_ENDPOINT_ATTR_NAME))
.ifPresent(uri -> conf.setOidcAuthorizationUri((String) uri));
Optional.ofNullable(response.get(TOKEN_ENDPOINT_ATTR_NAME))
.ifPresent(uri -> conf.setOidcTokenUri((String) uri));
Optional.ofNullable(response.get(USERINFO_ENDPOINT_ATTR_NAME))
.ifPresent(uri -> conf.setOidcUserInfoUri((String) uri));
Optional.ofNullable(response.get(JWK_SET_URI_ATTR_NAME)).ifPresent(uri -> conf.setOidcJwkSetUri((String) uri));
Optional.ofNullable(response.get(END_SESSION_ENDPONT)).ifPresent(uri -> conf.setOidcLogoutUri((String) uri));
Optional.ofNullable(response.get(SCOPES_SUPPORTED)).ifPresent(s -> {
@SuppressWarnings("unchecked")
List<String> scopes = (List<String>) s;
conf.setOidcScopes(collectScopes(scopes));
});
}
private String collectScopes(List<String> scopes) {
return scopes.stream().collect(Collectors.joining(" "));
}
}

View File

@ -0,0 +1,16 @@
<html>
<body>
<wicket:panel>
<label for="oidcDiscoveryUri">
<span>
<wicket:message key="discovery"></wicket:message>
</span>
</label>
<input id="oidcDiscoveryUri" wicket:id="oidcDiscoveryUri" type="text" class="field text" />
<button id="discover" wicket:id="discover" type="button">
<wicket:message key="discover"></wicket:message>
</button>
<a href="#" wicket:id="oidcDiscoveryUriKeyHelp" class="help-link"></a>
</wicket:panel>
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org/">
<body>
<wicket:panel>
<input wicket:id="tokenRolesClaim" class="field text" />
</wicket:panel>
</body>
</html>

View File

@ -0,0 +1,326 @@
<!--
~ (c) 2018 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org/">
<body>
<wicket:head>
<style type="text/css">
ul.horizontal div {
display: inline;
}
.checkLabelInline {
vertical-align: middle;
line-height: 2em;
display: flex;
align-items: center;
align-content: center;
flex-wrap: wrap;
label
{
padding
:
0
5px;
}
}
</style>
</wicket:head>
<wicket:extend>
<h3>
<wicket:message key="geoserverParameters"></wicket:message>
</h3>
<ul>
<li>
<fieldset>
<ul>
<li>
<label for="baseRedirectUri">
<span>
<wicket:message key="baseRedirectUri"></wicket:message>
</span>
</label>
<input id="baseRedirectUri" wicket:id="baseRedirectUri" type="text" class="field text" />
<a href="#" wicket:id="baseRedirectUriHelp" class="help-link"></a>
</li>
<li>
<label for="postLogoutRedirectUri">
<span>
<wicket:message key="postLogoutRedirectUri"></wicket:message>
</span>
</label>
<input id="postLogoutRedirectUri" wicket:id="postLogoutRedirectUri" type="text" class="field text" />
<a href="#" wicket:id="postLogoutRedirectUriHelp" class="help-link"></a>
</li>
<li class="choiceItem">
<input id="enableRedirectAuthenticationEntryPoint"
wicket:id="enableRedirectAuthenticationEntryPoint" type="checkbox"
/>
<label for="enableRedirectAuthenticationEntryPoint">
<span>
<wicket:message key="enableRedirectAuthenticationEntryPoint"></wicket:message>
</span>
</label>
<a href="#" wicket:id="enableRedirectAuthenticationEntryPointHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
</li>
</ul>
<br />
<div wicket:id="pfv">
<h3 wicket:id="providerHeadline"></h3>
<ul>
<li class="choiceItem">
<input wicket:id="enabled" type="checkbox" />
<label for="enabled">
<span>
<wicket:message key="enabled"></wicket:message>
</span>
</label>
</li>
</ul>
<div wicket:id="settings">
<ul>
<li>
<fieldset>
<legend>
<span wicket:id="infoFromProvider"> </span>
<a href="#" wicket:id="connectionFromParametersHelp" class="help-link"></a>
</legend>
<ul>
<li>
<label for="clientId">
<span>
<wicket:message key="clientId"></wicket:message>
</span>
</label>
<input id="clientId" wicket:id="clientId" type="text" class="field text" />
<a href="#" wicket:id="clientIdHelp" class="help-link"></a>
</li>
<li>
<label for="clientSecret">
<span>
<wicket:message key="clientSecret"></wicket:message>
</span>
</label>
<input id="clientSecret" wicket:id="clientSecret" type="text" class="field text" />
<a href="#" wicket:id="clientSecretHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
</li>
</ul>
<ul>
<li>
<fieldset>
<legend>
<span wicket:id="infoForProvider"> </span>
<a href="#" wicket:id="connectionForParametersHelp" class="help-link"></a>
</legend>
<ul>
<li>
<label for="redirectUri">
<span>
<wicket:message key="redirectUri"></wicket:message>
</span>
</label>
<input wicket:id="redirectUri" type="text" class="field text" />
<a href="#" wicket:id="redirectUriHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
</li>
</ul>
<ul>
<li>
<fieldset>
<legend>
<span>
<wicket:message key="protocolSettings"></wicket:message>
</span>
</legend>
<ul>
<li>
<label for="userNameAttribute">
<span>
<wicket:message key="userNameAttribute"></wicket:message>
</span>
</label>
<input wicket:id="userNameAttribute" type="text" class="field text" />
<a href="#" wicket:id="userNameAttributeHelp" class="help-link"></a>
</li>
<li wicket:id="displayOnScopeSupport">
<label for="scopes">
<span>
<wicket:message key="scopes"></wicket:message>
</span>
</label>
<input wicket:id="scopes" type="text" class="field text" />
<a href="#" wicket:id="scopesHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
<div wicket:id="displayOnOidc">
<fieldset>
<ul>
<li>
<div wicket:id="topPanel"></div>
</li>
<li>
<label for="oidcAuthorizationUri">
<span>
<wicket:message key="oidcAuthorizationUri"></wicket:message>
</span>
</label>
<input id="oidcAuthorizationUri" wicket:id="oidcAuthorizationUri" class="field text" />
<a href="#" wicket:id="oidcAuthorizationUriHelp" class="help-link"></a>
</li>
<li>
<label for="oidcTokenUri">
<span>
<wicket:message key="oidcTokenUri"></wicket:message>
</span>
</label>
<input id="oidcTokenUri" wicket:id="oidcTokenUri" type="text" class="field text" />
<a href="#" wicket:id="oidcTokenUriHelp" class="help-link"></a>
</li>
<li>
<label for="oidcUserInfoUri">
<span>
<wicket:message key="oidcUserInfoUri"></wicket:message>
</span>
</label>
<input id="oidcUserInfoUri" wicket:id="oidcUserInfoUri" type="text" class="field text" />
<a href="#" wicket:id="oidcUserInfoUriHelp" class="help-link"></a>
</li>
<li>
<label for="oidcJwkSetUri">
<span>
<wicket:message key="oidcJwkSetUri"></wicket:message>
</span>
</label>
<input id="oidcJwkSetUri" wicket:id="oidcJwkSetUri" type="text" class="field text" />
<a href="#" wicket:id="oidcJwkSetUriHelp" class="help-link"></a>
</li>
<li>
<label for="oidcLogoutUri">
<span>
<wicket:message key="oidcLogoutUri"></wicket:message>
</span>
</label>
<input id="oidcLogoutUri" wicket:id="oidcLogoutUri" type="text" class="field text" />
<a href="#" wicket:id="oidcLogoutUriHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
<fieldset>
<legend>
<span>
<wicket:message key="oidcAdvancedSettings"></wicket:message>
</span>
<a href="#" wicket:id="oidcAdvancedSettingsHelp" class="help-link"></a>
</legend>
<ul>
<li class="choiceItem">
<input id="oidcForceTokenUriHttps" wicket:id="oidcForceTokenUriHttps" type="checkbox" />
<label for="oidcForceTokenUriHttps">
<span>
<wicket:message key="oidcForceTokenUriHttps"></wicket:message>
</span>
</label>
</li>
<li class="choiceItem">
<input id="oidcForceAuthorizationUriHttps" wicket:id="oidcForceAuthorizationUriHttps"
type="checkbox"
/>
<label for="oidcForceAuthorizationUriHttps">
<span>
<wicket:message key="oidcForceAuthorizationUriHttps"></wicket:message>
</span>
</label>
</li>
<li class="choiceItem">
<input id="oidcEnforceTokenValidation" wicket:id="oidcEnforceTokenValidation"
type="checkbox"
/>
<label for="oidcEnforceTokenValidation">
<span>
<wicket:message key="oidcEnforceTokenValidation"></wicket:message>
</span>
</label>
<a href="#" wicket:id="oidcEnforceTokenValidationHelp" class="help-link"></a>
</li>
<li class="choiceItem">
<input id="oidcUsePKCE" wicket:id="oidcUsePKCE" type="checkbox" />
<label for="oidcUsePKCE">
<span>
<wicket:message key="oidcUsePKCE"></wicket:message>
</span>
</label>
<a href="#" wicket:id="oidcUsePKCEHelp" class="help-link"></a>
</li>
<li class="choiceItem">
<input id="oidcAllowUnSecureLogging" wicket:id="oidcAllowUnSecureLogging" type="checkbox" />
<label for="oidcAllowUnSecureLogging">
<span>
<wicket:message key="oidcAllowUnSecureLogging"></wicket:message>
</span>
</label>
<a href="#" wicket:id="oidcAllowUnSecureLoggingHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
<fieldset>
<legend>
<span>
<wicket:message key="oidcProviderSettings"></wicket:message>
</span>
<a href="#" wicket:id="oidcProviderSettingsHelp" class="help-link"></a>
</legend>
<ul>
<li>
<label for="oidcResponseMode">
<span>
<wicket:message key="oidcResponseMode"></wicket:message>
</span>
</label>
<input id="oidcResponseMode" wicket:id="oidcResponseMode" type="text" class="field text" />
<a href="#" wicket:id="oidcResponseModeHelp" class="help-link"></a>
</li>
<li class="choiceItem">
<input id="oidcAuthenticationMethodPostSecret"
wicket:id="oidcAuthenticationMethodPostSecret" type="checkbox"
/>
<label for="oidcAuthenticationMethodPostSecret">
<span>
<wicket:message key="oidcAuthenticationMethodPostSecret"></wicket:message>
</span>
</label>
<a href="#" wicket:id="oidcAuthenticationMethodPostSecretHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
</div>
</li>
</ul>
</div>
<br />
<br />
</div>
<h3>
<wicket:message key="authorization"></wicket:message>
</h3>
</wicket:extend>
</body>
</html>

View File

@ -0,0 +1,288 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.login;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource.AccessToken;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource.IdToken;
import static org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource.UserInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.model.StringResourceModel;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource;
import org.geoserver.security.web.auth.PreAuthenticatedUserNameFilterPanel;
import org.geoserver.security.web.auth.RoleSourceChoiceRenderer;
import org.geoserver.web.GeoServerBasePage;
import org.geoserver.web.wicket.GeoServerDialog;
import org.geoserver.web.wicket.HelpLink;
import org.geoserver.web.wicket.ParamResourceModel;
/**
* Configuration panel for {@link GeoServerOAuthAuthenticationFilter}.
*
* @author Alessio Fabiani, GeoSolutions S.A.S.
*/
public class OAuth2LoginAuthProviderPanel
extends PreAuthenticatedUserNameFilterPanel<GeoServerOAuth2LoginFilterConfig> {
/** serialVersionUID */
private static final long serialVersionUID = -3025321797363970333L;
/** Prefix of Microsoft specific attributes */
private static final String PREFIX_MS = "ms";
/** Prefix of GitHub specific attributes */
private static final String PREFIX_GIT_HUB = "gitHub";
/** Prefix of Google specific attributes */
private static final String PREFIX_GOOGLE = "google";
/** Prefix of custom OIDC specific attributes */
private static final String PREFIX_OIDC = "oidc";
private class DiscoveryPanel extends Panel {
private static final long serialVersionUID = 1L;
public DiscoveryPanel(String panelId) {
super(panelId);
TextField<String> url = new TextField<>(
"oidcDiscoveryUri", new PropertyModel<>(configModel.getObject(), "oidcDiscoveryUri"));
add(url);
add(new AjaxButton("discover") {
private static final long serialVersionUID = 1L;
@Override
protected void onError(AjaxRequestTarget target) {
onSubmit(target);
}
@Override
protected void onSubmit(AjaxRequestTarget target) {
url.processInput();
discover(url.getInput(), target);
}
});
add(new HelpLink("oidcDiscoveryUriKeyHelp", this).setDialog(dialog));
}
private void discover(String discoveryURL, AjaxRequestTarget target) {
GeoServerOAuth2LoginFilterConfig model = (GeoServerOAuth2LoginFilterConfig)
OAuth2LoginAuthProviderPanel.this.getForm().getModelObject();
try {
new DiscoveryClient(discoveryURL).autofill(model);
target.add(OAuth2LoginAuthProviderPanel.this);
((GeoServerBasePage) getPage()).addFeedbackPanels(target);
} catch (Exception e) {
error(new ParamResourceModel("discoveryError", this, e.getMessage()).getString());
((GeoServerBasePage) getPage()).addFeedbackPanels(target);
}
}
}
static class TokenClaimPanel extends Panel {
private static final long serialVersionUID = 1L;
public TokenClaimPanel(String id) {
super(id, new Model<>());
add(new TextField<String>("tokenRolesClaim").setRequired(true));
}
}
private GeoServerDialog dialog;
private List<Component> redirectUriComponents = new ArrayList<>();
@SuppressWarnings("serial")
public OAuth2LoginAuthProviderPanel(String id, IModel<GeoServerOAuth2LoginFilterConfig> model) {
super(id, model);
this.dialog = (GeoServerDialog) get("dialog");
add(new HelpLink("userNameAttributeHelp", this).setDialog(dialog));
add(new HelpLink("geoserverParametersHelp", this).setDialog(dialog));
TextField<String> tf = new TextField<>("baseRedirectUri");
add(tf);
add(new HelpLink("baseRedirectUriHelp", this).setDialog(dialog));
RepeatingView prefixView = new RepeatingView("pfv");
add(prefixView);
addProviderComponents(prefixView, PREFIX_GOOGLE, "Google");
addProviderComponents(prefixView, PREFIX_GIT_HUB, "GitHub");
addProviderComponents(prefixView, PREFIX_MS, "Microsoft Azure");
addProviderComponents(prefixView, PREFIX_OIDC, "OpenID Connect Provider");
tf.add(new AjaxFormComponentUpdatingBehavior("change") {
@Override
protected void onUpdate(AjaxRequestTarget pTarget) {
configModel.getObject().calculateRedirectUris();
redirectUriComponents.forEach(c -> {
String lid = c.getMarkupId();
pTarget.add(c, lid);
});
}
});
add(new HelpLink("enableRedirectAuthenticationEntryPointHelp", this).setDialog(dialog));
add(new CheckBox("enableRedirectAuthenticationEntryPoint"));
add(new HelpLink("connectionParametersHelp", this).setDialog(dialog));
add(new HelpLink("postLogoutRedirectUriHelp", this).setDialog(dialog));
add(new TextField<>("postLogoutRedirectUri"));
}
private void addProviderComponents(RepeatingView pView, String pProviderKey, String pProviderLabel) {
WebMarkupContainer lContainer = new WebMarkupContainer(pView.newChildId());
pView.add(lContainer);
lContainer.add(createLabelResourceWithParams("providerHeadline", pProviderLabel));
WebMarkupContainer lSHContainer = new WebMarkupContainer("settings");
lSHContainer.setOutputMarkupId(true);
lContainer.add(lSHContainer);
IModel<Boolean> lModel = new PropertyModel<>(configModel.getObject(), pProviderKey + "Enabled");
CheckBox cb = new CheckBox("enabled", lModel);
lContainer.add(cb);
cb.add(new ToggleDisplayCheckboxBehavior(lSHContainer));
lSHContainer.add(createLabelResourceWithParams("infoFromProvider", pProviderLabel));
lSHContainer.add(createLabelResourceWithParams("infoForProvider", pProviderLabel));
lSHContainer.add(new HelpLink("connectionFromParametersHelp", this).setDialog(dialog));
lSHContainer.add(createTextField("clientId", pProviderKey));
lSHContainer.add(new HelpLink("clientIdHelp", this).setDialog(dialog));
lSHContainer.add(createTextField("clientSecret", pProviderKey));
lSHContainer.add(new HelpLink("clientSecretHelp", this).setDialog(dialog));
lSHContainer.add(createTextField("userNameAttribute", pProviderKey));
lSHContainer.add(new HelpLink("userNameAttributeHelp", this).setDialog(dialog));
TextField<String> lRedirectUriField = createTextField("redirectUri", pProviderKey, false);
lRedirectUriField.setOutputMarkupId(true);
redirectUriComponents.add(lRedirectUriField);
lSHContainer.add(lRedirectUriField);
lSHContainer.add(new HelpLink("connectionForParametersHelp", this).setDialog(dialog));
lSHContainer.add(new HelpLink("redirectUriHelp", this).setDialog(dialog));
// -- Provider specifics below --
boolean lSupportsScope = pProviderKey.equals(PREFIX_MS) || pProviderKey.equals(PREFIX_OIDC);
WebMarkupContainer lScopeContainer = new WebMarkupContainer("displayOnScopeSupport");
lSHContainer.add(lScopeContainer);
if (lSupportsScope) {
lScopeContainer.add(createTextField("scopes", pProviderKey));
lScopeContainer.add(new HelpLink("scopesHelp", this).setDialog(dialog));
} else {
lScopeContainer.setVisible(false);
}
boolean lOidc = pProviderKey.equals(PREFIX_OIDC);
WebMarkupContainer lOidcContainer = new WebMarkupContainer("displayOnOidc");
lSHContainer.add(lOidcContainer);
if (lOidc) {
lOidcContainer.add(new DiscoveryPanel("topPanel"));
lOidcContainer.add(new HelpLink("oidcTokenUriHelp", this).setDialog(dialog));
lOidcContainer.add(new HelpLink("oidcAuthorizationUriHelp", this).setDialog(dialog));
lOidcContainer.add(new HelpLink("oidcUserInfoUriHelp", this).setDialog(dialog));
lOidcContainer.add(new CheckBox("oidcForceAuthorizationUriHttps"));
lOidcContainer.add(new CheckBox("oidcForceTokenUriHttps"));
lOidcContainer.add(new TextField<>("oidcTokenUri"));
lOidcContainer.add(new TextField<>("oidcAuthorizationUri"));
lOidcContainer.add(new TextField<>("oidcUserInfoUri"));
lOidcContainer.add(new HelpLink("oidcJwkSetUriHelp", this).setDialog(dialog));
lOidcContainer.add(new TextField<>("oidcJwkSetUri"));
lOidcContainer.add(new HelpLink("oidcResponseModeHelp", this).setDialog(dialog));
lOidcContainer.add(new TextField<>("oidcResponseMode"));
lOidcContainer.add(new HelpLink("oidcEnforceTokenValidationHelp", this).setDialog(dialog));
lOidcContainer.add(new CheckBox("oidcEnforceTokenValidation"));
lOidcContainer.add(new HelpLink("oidcAuthenticationMethodPostSecretHelp", this).setDialog(dialog));
lOidcContainer.add(new CheckBox("oidcAuthenticationMethodPostSecret"));
lOidcContainer.add(new HelpLink("oidcUsePKCEHelp", this).setDialog(dialog));
lOidcContainer.add(new CheckBox("oidcUsePKCE"));
lOidcContainer.add(new HelpLink("oidcAllowUnSecureLoggingHelp", this).setDialog(dialog));
lOidcContainer.add(new CheckBox("oidcAllowUnSecureLogging"));
lOidcContainer.add(new HelpLink("oidcLogoutUriHelp", this).setDialog(dialog));
lOidcContainer.add(new TextField<>("oidcLogoutUri"));
lOidcContainer.add(new HelpLink("oidcAdvancedSettingsHelp", this).setDialog(dialog));
lOidcContainer.add(new HelpLink("oidcProviderSettingsHelp", this).setDialog(dialog));
} else {
lOidcContainer.setVisible(false);
}
}
/**
* @param pKey
* @param pParams
* @return a {@link Label} with {@link StringResourceModel} and parameters set
*/
private Label createLabelResourceWithParams(String pKey, Object... pParams) {
StringResourceModel lModel = new StringResourceModel(pKey);
lModel.setParameters(pParams);
Label lLabel = new Label(pKey, lModel);
return lLabel;
}
private TextField<String> createTextField(String pFieldName, String pProviderName) {
return createTextField(pFieldName, pProviderName, true);
}
private TextField<String> createTextField(String pAttr, String pProvider, boolean pEnabled) {
String lModelField = pProvider + StringUtils.capitalize(pAttr);
IModel<String> lModel = new PropertyModel<>(configModel.getObject(), lModelField);
TextField<String> lTextField = new TextField<>(pAttr, lModel);
lTextField.setEnabled(pEnabled);
return lTextField;
}
@Override
protected Panel getRoleSourcePanel(RoleSource model) {
if (IdToken.equals(model) || AccessToken.equals(model) || UserInfo.equals(model)) {
return new TokenClaimPanel("panel");
}
return super.getRoleSourcePanel(model);
}
@Override
protected DropDownChoice<RoleSource> createRoleSourceDropDown() {
List<RoleSource> sources = new ArrayList<>(Arrays.asList(OpenIdRoleSource.values()));
sources.addAll(Arrays.asList(PreAuthenticatedUserNameRoleSource.values()));
return new DropDownChoice<>("roleSource", sources, new RoleSourceChoiceRenderer());
}
public IModel<GeoServerOAuth2LoginFilterConfig> getConfigModel() {
return this.configModel;
}
}

View File

@ -0,0 +1,23 @@
/* (c) 2018 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.login;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geoserver.security.web.auth.AuthenticationFilterPanelInfo;
/** Configuration panel extension for {@link GeoServerOAuthAuthenticationFilter}. */
public class OAuth2LoginAuthProviderPanelInfo
extends AuthenticationFilterPanelInfo<GeoServerOAuth2LoginFilterConfig, OAuth2LoginAuthProviderPanel> {
/** serialVersionUID */
private static final long serialVersionUID = -3891569684560944819L;
public OAuth2LoginAuthProviderPanelInfo() {
setComponentClass(OAuth2LoginAuthProviderPanel.class);
setServiceClass(GeoServerOAuth2LoginAuthenticationFilter.class);
setServiceConfigClass(GeoServerOAuth2LoginFilterConfig.class);
}
}

View File

@ -0,0 +1,40 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.login;
import java.util.ArrayList;
import java.util.List;
import org.geoserver.security.oauth2.login.OAuth2LoginButtonEnablementEvent;
import org.geoserver.web.LoginFormInfo;
import org.springframework.context.event.EventListener;
/**
* Enables login buttons for OAuth2 dynamically, depending on the respective providers enablement state. Required since
* a single filter instance supports multiple providers, so the regular enablement of login buttons based on the
* presence of a filter is not sufficient.
*/
public class OAuth2LoginButtonManager {
private List<LoginFormInfo> loginFormInfos = new ArrayList<>();
public OAuth2LoginButtonManager() {
super();
}
@EventListener
public void enablementChanged(OAuth2LoginButtonEnablementEvent pEvent) {
String lRegId = pEvent.getRegistrationId().toLowerCase();
for (LoginFormInfo lInfo : loginFormInfos) {
if (lInfo.getId() != null && lInfo.getId().toLowerCase().contains(lRegId)) {
lInfo.setEnabled(pEvent.isEnable());
}
}
}
/** @param pInfos the infos to set */
public void setLoginFormInfos(List<LoginFormInfo> pInfos) {
loginFormInfos = pInfos;
}
}

View File

@ -0,0 +1,60 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.login;
import static java.lang.Boolean.TRUE;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.StyleAttributeModifier;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.form.CheckBox;
/**
* Behavior for {@link CheckBox} components allows to toggle visibility of another {@link #targetComponent}. The
* visibility is based on the CSS display=none property so that the regular form processing in unaffected.
*
* <p>Circumvents problems with Ajax based solutions which tend to loose intermediate user input in target components on
* re-render/hide.
*
* @author awaterme
*/
public class ToggleDisplayCheckboxBehavior extends Behavior {
private static final long serialVersionUID = 1L;
private final Component targetComponent;
public ToggleDisplayCheckboxBehavior(Component targetComponent) {
this.targetComponent = targetComponent;
}
@Override
public void bind(Component component) {
super.bind(component);
targetComponent.add(new StyleAttributeModifier() {
private static final long serialVersionUID = 1L;
@Override
protected Map<String, String> update(Map<String, String> pOldStyles) {
CheckBox cb = (CheckBox) component;
Boolean lChecked = TRUE.equals(cb.getModelObject());
pOldStyles.put("display", lChecked ? "block" : "none");
return pOldStyles;
}
});
}
@Override
public void onComponentTag(Component component, ComponentTag tag) {
super.onComponentTag(component, tag);
String onchangeScript = String.format(
"document.getElementById('%s').style.display = this.checked ? 'block' : 'none' ;",
targetComponent.getMarkupId(true));
tag.put("onchange", onchangeScript);
}
}

View File

@ -0,0 +1,43 @@
<!--
~ (c) 2018 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.apache.org/">
<body>
<wicket:head>
<style type="text/css">
ul.horizontal div {
display:inline;
}
</style>
</wicket:head>
<wicket:extend>
<ul>
<li>
<fieldset>
<legend>
<span><wicket:message key="resourceServerParameters"></wicket:message></span>
<a href="#" wicket:id="resourceServerParametersHelp" class="help-link"></a>
</legend>
<ul>
<li>
<label for="issuerUri"><span><wicket:message key="issuerUri"></wicket:message></span></label>
<input id="issuerUri" wicket:id="issuerUri" type="text" class="field text"/>
<a href="#" wicket:id="issuerUriHelp" class="help-link"></a>
</li>
</ul>
</fieldset>
</li>
</ul>
</wicket:extend>
</body>
</html>

View File

@ -0,0 +1,63 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.resourceserver;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig.OpenIdRoleSource;
import org.geoserver.security.oauth2.resourceserver.GeoServerOAuth2ResourceServerFilterConfig;
import org.geoserver.security.web.auth.PreAuthenticatedUserNameFilterPanel;
import org.geoserver.security.web.auth.RoleSourceChoiceRenderer;
import org.geoserver.web.wicket.GeoServerDialog;
import org.geoserver.web.wicket.HelpLink;
/**
* Configuration panel for {@link GeoServerOAuthAuthenticationFilter}.
*
* @author Alessio Fabiani, GeoSolutions S.A.S.
*/
public class OAuth2ResourceServerAuthProviderPanel
extends PreAuthenticatedUserNameFilterPanel<GeoServerOAuth2ResourceServerFilterConfig> {
/** serialVersionUID */
private static final long serialVersionUID = -3025321797363970333L;
private GeoServerDialog dialog;
public OAuth2ResourceServerAuthProviderPanel(String id, IModel<GeoServerOAuth2ResourceServerFilterConfig> model) {
super(id, model);
this.dialog = (GeoServerDialog) get("dialog");
add(new HelpLink("resourceServerParametersHelp", this).setDialog(dialog));
add(new TextField<>("issuerUri"));
add(new HelpLink("issuerUriHelp", this).setDialog(dialog));
}
@Override
protected void onInitialize() {
super.onInitialize();
}
@Override
protected Panel getRoleSourcePanel(RoleSource model) {
return super.getRoleSourcePanel(model);
}
@Override
protected DropDownChoice<RoleSource> createRoleSourceDropDown() {
List<RoleSource> sources = new ArrayList<>(Arrays.asList(OpenIdRoleSource.values()));
sources.addAll(Arrays.asList(PreAuthenticatedUserNameRoleSource.values()));
return new DropDownChoice<>("roleSource", sources, new RoleSourceChoiceRenderer());
}
}

View File

@ -0,0 +1,24 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.resourceserver;
import org.geoserver.security.oauth2.resourceserver.GeoServerOAuth2ResourceServerAuthenticationFilter;
import org.geoserver.security.oauth2.resourceserver.GeoServerOAuth2ResourceServerFilterConfig;
import org.geoserver.security.web.auth.AuthenticationFilterPanelInfo;
/** Configuration panel extension for {@link GeoServerOAuthAuthenticationFilter}. */
public class OAuth2ResourceServerAuthProviderPanelInfo
extends AuthenticationFilterPanelInfo<
GeoServerOAuth2ResourceServerFilterConfig, OAuth2ResourceServerAuthProviderPanel> {
/** serialVersionUID */
private static final long serialVersionUID = -3891569684560944819L;
public OAuth2ResourceServerAuthProviderPanelInfo() {
setComponentClass(OAuth2ResourceServerAuthProviderPanel.class);
setServiceClass(GeoServerOAuth2ResourceServerAuthenticationFilter.class);
setServiceConfigClass(GeoServerOAuth2ResourceServerFilterConfig.class);
}
}

View File

@ -0,0 +1,188 @@
#
# (c) 2018 Open Source Geospatial Foundation - all rights reserved
# This code is licensed under the GPL 2.0 license, available at the root
# application directory.
#
org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter.name=OAuth2 / OpenID Connect Login
org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter.title=Interactive authentication using OAuth2 or OpenID Connect
OAuth2LoginAuthProviderPanel.description=Interactive authentication using OAuth2 or OpenID Connect
OAuth2LoginAuthProviderPanel.geoserverParameters=Common Login Settings
OAuth2LoginAuthProviderPanel.baseRedirectUri=Redirect Base URI
OAuth2LoginAuthProviderPanel.baseRedirectUriHelp.title=Redirect Base URI
OAuth2LoginAuthProviderPanel.baseRedirectUriHelp=Specifies the URI under which the GeoServer \
can be reached by users who want to log in with OAuth2 / OpenID Connect. \
<p>A public URI is not necessarily required, but the URI must be equally accessible for these users.</p>\
<p>The URI must end with the context path used by the GeoServer, typically "/geoserver". </p>\
<p>After entering the URI, the provider-specific redirect URIs listed below are automatically updated.</p>
OAuth2LoginAuthProviderPanel.postLogoutRedirectUri=After-Logout Redirect URI (Register with the provider)
OAuth2LoginAuthProviderPanel.postLogoutRedirectUriHelp.title=After-Logout Redirect URI
OAuth2LoginAuthProviderPanel.postLogoutRedirectUriHelp=The URI to which the user should be \
redirected from the OAuth2 / OpenID provider once a global logout has been completed. \
<p>Attention: When registering the GeoServer as an application with the OAuth2 / OpenID \
Connect provider, the URIs to which the provider may forward the user must be specified. \
If you store a URI here, it must also be stored as a permitted URI with the provider.</p>
OAuth2LoginAuthProviderPanel.enableRedirectAuthenticationEntryPoint=Skip GeoServer login dialog
OAuth2LoginAuthProviderPanel.enableRedirectAuthenticationEntryPointHelp.title=Skip GeoServer login dialog
OAuth2LoginAuthProviderPanel.enableRedirectAuthenticationEntryPointHelp=Activate this option to \
redirect users who are not logged in directly to the login page of the OAuth2 / OpenID Connect \
Provider. The GeoServer login page is skipped. <p>If only one provider is activated and this \
provider is to be used in all cases, this option can be used.</p><p>Attention: This means that \
it is no longer possible to log in with a local account. Affects the FilterChains for \
which this filter is activated.</p><p>This is only a convenience function. If in doubt, \
leave deactivated.</p>
OAuth2LoginAuthProviderPanel.enabled=Enabled
OAuth2LoginAuthProviderPanel.providerHeadline={0} Login
OAuth2LoginAuthProviderPanel.infoFromProvider=Information from {0}
OAuth2LoginAuthProviderPanel.infoForProvider=Information for {0}
OAuth2LoginAuthProviderPanel.connectionFromParametersHelp.title=Information from Provider
OAuth2LoginAuthProviderPanel.connectionFromParametersHelp=The GeoServer must be registered as \
an application with the OAuth2 / OpenID Connect provider by an authorized administrator. \
The ClientId and ClientSecret are defined during registration. \
This information must be communicated to the GeoServer.
OAuth2LoginAuthProviderPanel.connectionForParametersHelp.title=Information for Provider
OAuth2LoginAuthProviderPanel.connectionForParametersHelp=When registering the GeoServer as \
an application with the OAuth2 / OpenID Connect provider, some information must be provided, \
including the information specified in this section.
OAuth2LoginAuthProviderPanel.protocolSettings=Protocol Settings
OAuth2LoginAuthProviderPanel.userNameAttribute=User name attribute
OAuth2LoginAuthProviderPanel.userNameAttributeHelp.title=Key identifying the user name
OAuth2LoginAuthProviderPanel.userNameAttributeHelp=The OAuth2 server replies to user verification with a JSON document, \
this entry specifies with JSON key should be used as user name in GeoServer
OAuth2LoginAuthProviderPanel.oidcAdvancedSettings=Advanced Settings
OAuth2LoginAuthProviderPanel.oidcAdvancedSettingsHelp.title=Advanced Settings
OAuth2LoginAuthProviderPanel.oidcAdvancedSettingsHelp=In most cases, the default values can be retained.
OAuth2LoginAuthProviderPanel.oidcProviderSettings=Provider-specific Settings
OAuth2LoginAuthProviderPanel.oidcProviderSettingsHelp.title=Provider-specific Settings
OAuth2LoginAuthProviderPanel.oidcProviderSettingsHelp=Some OpenID Connect providers require a special configuration, which can be carried out here.
# Login buttons tool tips
OAuth2LoginAuthProviderPanel.googleDescription=Login with Google
OAuth2LoginAuthProviderPanel.gitHubDescription=Login with GitHub
OAuth2LoginAuthProviderPanel.msDescription=Login with Microsoft Azure
OAuth2LoginAuthProviderPanel.oidcDescription=Login with OpenID Connect
OAuth2LoginAuthProviderPanel.login=OpenID Connect Login
OAuth2LoginAuthProviderPanel.logout=OpenID Connect Logout
OAuth2LoginAuthProviderPanel.short=OpenID Connect Login
OAuth2LoginAuthProviderPanel.title=Authentication using OpenID Connect
OAuth2LoginAuthProviderPanel.connectionParameters=OpenID Connect provider connection
OAuth2LoginAuthProviderPanel.connectionParametersHelp.title=OpenID Connect provider connection
OAuth2LoginAuthProviderPanel.connectionParametersHelp=<p>The URIs of the OpenID Connect token service and user authorization.</p>
OAuth2LoginAuthProviderPanel.oidcForceTokenUriHttps=Force Access Token URI HTTPS Secured Protocol
OAuth2LoginAuthProviderPanel.oidcTokenUri=Access Token URI
OAuth2LoginAuthProviderPanel.oidcTokenUriHelp.title=Access Token URI
OAuth2LoginAuthProviderPanel.oidcTokenUriHelp=The URI to use to obtain an OAuth2 access token.
OAuth2LoginAuthProviderPanel.oidcForceAuthorizationUriHttps=Force User Authorization URI HTTPS Secured Protocol
OAuth2LoginAuthProviderPanel.oidcAuthorizationUri=User Authorization URI
OAuth2LoginAuthProviderPanel.oidcAuthorizationUriHelp.title=User Authorization URI
OAuth2LoginAuthProviderPanel.oidcAuthorizationUriHelp=The URI to which the user is to be redirected to authorize an access token.
OAuth2LoginAuthProviderPanel.redirectUri=Redirect URI
OAuth2LoginAuthProviderPanel.redirectUriHelp.title=Redirect URI
OAuth2LoginAuthProviderPanel.redirectUriHelp=When registering the GeoServer as an application \
with the OAuth2 / OpenID Connect provider, it must be specified to which URI the provider \
may forward the user if the login was successful. Use the adjacent URI for this purpose.\
<p>The URI is automatically determined from the "Redirect Base URI" entered above. \
Adjust the "Redirect Base URI" accordingly. </p><p>Background: The GeoServer uses a specific \
URI for each active provider in order to be able to assign the login to a provider.</p>
OAuth2LoginAuthProviderPanel.oidcUserInfoUri=User Info URI
OAuth2LoginAuthProviderPanel.oidcUserInfoUriHelp.title=User Info URI
OAuth2LoginAuthProviderPanel.oidcUserInfoUriHelp=Used to load user information in case of an OAuth2 provider not supporting to issue OpenID Connect ID-Tokens. validate the <b>access_token</b>
OAuth2LoginAuthProviderPanel.oidcLogoutUri=Logout URI
OAuth2LoginAuthProviderPanel.oidcLogoutUriHelp.title=Logout URI
OAuth2LoginAuthProviderPanel.oidcLogoutUriHelp=The URI to which the user is to be redirected when performing a logout.
OAuth2LoginAuthProviderPanel.scopes=Scopes
OAuth2LoginAuthProviderPanel.scopesHelp.title=Scopes
OAuth2LoginAuthProviderPanel.scopesHelp=<p>The comma-separated scopes of this resource.</p> \
<p>Scopes are needed in order to ask the OAuth2 Provider for user details, which will be used to authorize him.</p>
OAuth2LoginAuthProviderPanel.clientId=Client ID
OAuth2LoginAuthProviderPanel.clientIdHelp.title=Client ID
OAuth2LoginAuthProviderPanel.clientIdHelp=The client identifier to use for this protected resource.
OAuth2LoginAuthProviderPanel.clientSecret=Client Secret
OAuth2LoginAuthProviderPanel.clientSecretHelp.title=Client Secret
OAuth2LoginAuthProviderPanel.clientSecretHelp=The client secret key, provided by the OAuth2 Provider.
DiscoveryPanel.discovery=OpenID Discovery document
DiscoveryPanel.discover=Discover
DiscoveryPanel.oidcDiscoveryUriKeyHelp.title=Discovery
DiscoveryPanel.oidcDiscoveryUriKeyHelp=<p>Automatically fill in the connection parameters from a OpenID Discovery document</p>
DiscoveryPanel.discoveryError=Could not look-up discovery information: {0}
OAuth2LoginAuthProviderPanel.oidcJwkSetUri=JSON Web Key Set URI
OAuth2LoginAuthProviderPanel.oidcJwkSetUriHelp.title=JSON Web Key Set URI
OAuth2LoginAuthProviderPanel.oidcJwkSetUriHelp=Link to a set of JSON Web Keys, as a JSON document. Used to validate the Id Token signature.
OAuth2LoginAuthProviderPanel.oidcEnforceTokenValidation=Enforce Token Validation
OAuth2LoginAuthProviderPanel.oidcEnforceTokenValidationHelp.title=Enforce Token Validation
OAuth2LoginAuthProviderPanel.oidcEnforceTokenValidationHelp=Check this option to enforce the validation of the token signature.
OAuth2LoginAuthProviderPanel.oidcResponseMode=Response Mode
OAuth2LoginAuthProviderPanel.oidcResponseModeHelp.title=Response Mode
OAuth2LoginAuthProviderPanel.oidcResponseModeHelp=Tells the OpenID Connect provider how \
to transfer the authorization code. GeoServer requires a transmission as query parameter. \
<p>For some providers, this transmission type must be set explicitly. Currently, for example, \
the Active Directory Foundation Services (ADFS). Enter "query" for these providers.</p>
OAuth2LoginAuthProviderPanel.oidcAuthenticationMethodPostSecret=Send Client Secret in Token Request as POST
OAuth2LoginAuthProviderPanel.oidcAuthenticationMethodPostSecretHelp.title=Send Client Secret in Token Request as POST
OAuth2LoginAuthProviderPanel.oidcAuthenticationMethodPostSecretHelp= Check this option if the \
OpenID Connect provider requires GeoServer to send the client secret as part of the POST body \
in the token request (e.g. ADFS) rather than basic authentication, which is the default.
OAuth2LoginAuthProviderPanel.oidcUsePKCE=Proof Key of Code Exchange
OAuth2LoginAuthProviderPanel.oidcUsePKCE=Use PKCE
OAuth2LoginAuthProviderPanel.oidcUsePKCEHelp.title=Use Proof Key of Code Exchange
OAuth2LoginAuthProviderPanel.oidcUsePKCEHelp=Use Proof Key of Code Exchange (PKE) as an additional guard against code interception.\
The client generates a code_verifier to be added to the OAuth authorization URI. The code_verifier is used in the final \
authorization code for token exchange. The use of PKCE is recommended in cases where client code is public and use of \
client id and a client secret may be discovered.
OAuth2LoginAuthProviderPanel.oidcAllowUnSecureLogging=Log Sensitive Information (do not use in production)
OAuth2LoginAuthProviderPanel.oidcAllowUnSecureLoggingHelp.title=Log Sensitive Information
OAuth2LoginAuthProviderPanel.oidcAllowUnSecureLoggingHelp=Check this option if you want to log more sensitive information \
(i.e. tokens). This is useful when debugging an OIDC configuration that isn't working correctly.
OAuth2LoginAuthProviderPanel.authorization=Authorization
RoleSource.IdToken=ID Token
RoleSource.AccessToken=Access Token
RoleSource.UserInfo=UserInfo Endpoint
RoleSource.MSGraphAPI=Microsoft Graph API (Entra ID)
TokenClaimPanel=Token roles claim
org.geoserver.web.security.oauth2.OAuth2ResourceServerAuthProviderPanel.name=OAuth2 Resource Server
org.geoserver.web.security.oauth2.OAuth2ResourceServerAuthProviderPanel.title=Authentication using OAuth2 Bearer Tokens
OAuth2ResourceServerAuthProviderPanel.short=OAuth2 Resource Server
OAuth2ResourceServerAuthProviderPanel.title=OAuth2 Resource Server Authentication
OAuth2ResourceServerAuthProviderPanel.description=Turn GeoServer into OAuth2 Resource Server using Bearer Tokens
OAuth2ResourceServerAuthProviderPanel.resourceServerParameters=Resource Server properties
OAuth2ResourceServerAuthProviderPanel.resourceServerParametersHelp=TODO
OAuth2ResourceServerAuthProviderPanel.resourceServerParametersHelp.title=TODO
OAuth2ResourceServerAuthProviderPanel.issuerUri=Issuer URI
OAuth2ResourceServerAuthProviderPanel.issuerUriHelp=TODO
OAuth2ResourceServerAuthProviderPanel.issuerUriHelp.title=TODO

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ (c) 2018 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:sec="http://www.springframework.org/schema/security" xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.4.xsd
http://www.springframework.org/schema/security/oauth2
http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd">
<bean id="openIdConnectWebExtension"
class="org.geoserver.platform.ModuleStatusImpl">
<constructor-arg index="0" value="gs-sec-oidc-web" />
<constructor-arg index="1"
value="GeoServer Web UI Security OpenID Connect" />
</bean>
<!-- ui auth provider panel info -->
<bean id="openIdConnectAuthPanelInfo"
class="org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanelInfo">
<property name="id" value="security.GeoServerOAuth2LoginAuthenticationProvider" />
<property name="shortTitleKey" value="OAuth2LoginAuthProviderPanel.short" />
<property name="titleKey" value="OAuth2LoginAuthProviderPanel.title" />
<property name="descriptionKey" value="OAuth2LoginAuthProviderPanel.description" />
</bean>
<!--
Used for the "Resource Server" use case. Implementation is unfinished, because a different GS
extension supports this case already. Filter is not offered in UI. This code is never executed.
<bean id="oauth2ResourceServerAuthPanelInfo"
class="org.geoserver.web.security.oauth2.resourceserver.OAuth2ResourceServerAuthProviderPanelInfo">
<property name="id" value="security.GeoServerOAuth2ResourceServerAuthenticationProvider" />
<property name="shortTitleKey" value="OAuth2ResourceServerAuthProviderPanel.short" />
<property name="titleKey" value="OAuth2ResourceServerAuthProviderPanel.title" />
<property name="descriptionKey" value="OAuth2ResourceServerAuthProviderPanel.description" />
</bean>
-->
<!-- login buttons -->
<bean id="openIdConnectGoogleLoginButton" class="org.geoserver.web.LoginFormInfo">
<!-- id must contain Spring oauthClient registrationId for enablement to work -->
<!-- see GeoServerOAuth2LoginAuthenticationProvider and OAuth2LoginButtonManager-->
<property name="id" value="openIdConnectGoogleLoginButton" />
<property name="titleKey" value="" />
<property name="descriptionKey" value="OAuth2LoginAuthProviderPanel.googleDescription" />
<property name="componentClass" value="org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanel" />
<property name="name" value="openidconnect-google" />
<property name="icon" value="google.png" />
<property name="filterClass" value="org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter" />
<property name="loginPath" value="/oauth2/authorization/google" />
<property name="method" value="GET" />
<property name="enabled" value="false" />
</bean>
<bean id="openIdConnectGitHubLoginButton" class="org.geoserver.web.LoginFormInfo">
<!-- id must contain Spring oauthClient registrationId for enablement to work -->
<property name="id" value="openIdConnectGitHubLoginButton" />
<property name="titleKey" value="" />
<property name="descriptionKey" value="OAuth2LoginAuthProviderPanel.gitHubDescription" />
<property name="componentClass" value="org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanel" />
<property name="name" value="openidconnect-github" />
<property name="icon" value="github.png" />
<property name="filterClass" value="org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter" />
<property name="loginPath" value="/oauth2/authorization/gitHub" />
<property name="method" value="GET" />
<property name="enabled" value="false" />
</bean>
<bean id="openIdConnectMicrosoftLoginButton" class="org.geoserver.web.LoginFormInfo">
<!-- id must contain Spring oauthClient registrationId for enablement to work -->
<property name="id" value="openIdConnectMicrosoftLoginButton" />
<property name="titleKey" value="" />
<property name="descriptionKey" value="OAuth2LoginAuthProviderPanel.msDescription" />
<property name="componentClass" value="org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanel" />
<property name="name" value="openidconnect-microsoft" />
<property name="icon" value="microsoft.png" />
<property name="filterClass" value="org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter" />
<property name="loginPath" value="/oauth2/authorization/microsoft" />
<property name="method" value="GET" />
<property name="enabled" value="false" />
</bean>
<bean id="openIdConnectOidcLoginButton" class="org.geoserver.web.LoginFormInfo">
<!-- id must contain Spring oauthClient registrationId for enablement to work -->
<property name="id" value="openIdConnectOidcLoginButton" />
<property name="titleKey" value="" />
<property name="descriptionKey" value="OAuth2LoginAuthProviderPanel.oidcDescription" />
<property name="componentClass" value="org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanel" />
<property name="name" value="openidconnect-oidc" />
<property name="icon" value="openid.png" />
<property name="filterClass" value="org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter" />
<property name="loginPath" value="/oauth2/authorization/oidc" />
<property name="method" value="GET" />
<property name="enabled" value="false" />
</bean>
<bean id="oauth2LoginButtonManager" class="org.geoserver.web.security.oauth2.login.OAuth2LoginButtonManager" lazy-init="false">
<property name="loginFormInfos">
<list>
<ref bean="openIdConnectGoogleLoginButton"/>
<ref bean="openIdConnectGitHubLoginButton"/>
<ref bean="openIdConnectMicrosoftLoginButton"/>
<ref bean="openIdConnectOidcLoginButton"/>
</list>
</property>
</bean>
</beans>

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

View File

@ -0,0 +1,250 @@
/*
* (c) 2018 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.web.security.oauth2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import org.apache.wicket.Component;
import org.apache.wicket.model.Model;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geoserver.security.web.AbstractSecurityNamedServicePanelTest;
import org.geoserver.security.web.AbstractSecurityPage;
import org.geoserver.security.web.SecurityNamedServiceNewPage;
import org.geoserver.security.web.auth.AuthenticationPage;
import org.geoserver.web.FormTestPage;
import org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanel;
import org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanelInfo;
import org.junit.Before;
import org.junit.Test;
/** Tests for {@link OAuth2LoginAuthProviderPanel} */
public class OAuth2LoginAuthProviderPanelTest extends AbstractSecurityNamedServicePanelTest {
@Before
public void setup() {
System.setProperty(GeoServerOAuth2LoginFilterConfig.OPENID_TEST_GS_PROXY_BASE, "http://localhost/geoserver");
}
@Test
public void smokeTest() {
Model<GeoServerOAuth2LoginFilterConfig> model = new Model<>(new GeoServerOAuth2LoginFilterConfig());
FormTestPage testPage = new FormTestPage(id -> new OAuth2LoginAuthProviderPanel(id, model));
tester.startPage(testPage);
}
/**
* Creates a new configuration for a {@link GeoServerOAuth2LoginFilterConfig} providing all user input and verifies
* the configuration object contains the input after saving and reopening. Further steps change the user input and
* verify changes are also written to configuration.
*
* @throws Exception
*/
@Test
public void testUserInputSaveModify() throws Exception {
String filterName = "OpenIdFilter1";
navigateToOpenIdPanel(filterName);
// redirectUri Ajax test
// Unfortunately wicketTester forgets existing form data on ajax request, even if not submitting.
// So input has to provided twice. I think is a wicketTester bug...
String prefix = "panel:content:";
String baseUrl = "https://localhost:9090";
String baseUrlComponentPath = prefix + "baseRedirectUri";
formTester.setValue(baseUrlComponentPath, baseUrl + "/geoserver");
Component lComponent = formTester.getForm().get(baseUrlComponentPath);
tester.executeAjaxEvent(lComponent, "change");
formTester.setValue(baseUrlComponentPath, baseUrl + "/geoserver");
// filter
formTester.setValue(prefix + "name", filterName);
// common
formTester.setValue(prefix + "postLogoutRedirectUri", baseUrl + "/geoserver/postlogout");
formTester.setValue(prefix + "enableRedirectAuthenticationEntryPoint", false);
// Google
prefix = "panel:content:pfv:1:";
setBasicProviderValues(prefix, "google");
// GitHub
prefix = "panel:content:pfv:2:";
setBasicProviderValues(prefix, "gitHub");
// Microsoft
prefix = "panel:content:pfv:3:";
setBasicProviderValues(prefix, "ms");
prefix = prefix + "settings:";
formTester.setValue(prefix + "displayOnScopeSupport:scopes", "msScopes");
// OIDC
prefix = "panel:content:pfv:4:";
setBasicProviderValues(prefix, "oidc");
prefix = prefix + "settings:";
formTester.setValue(prefix + "displayOnScopeSupport:scopes", "oidcScopes");
String authUrl = "https://localhost:9000";
formTester.setValue(prefix + "displayOnOidc:oidcTokenUri", authUrl + "/token");
formTester.setValue(prefix + "displayOnOidc:oidcAuthorizationUri", authUrl + "/authorize");
formTester.setValue(prefix + "displayOnOidc:oidcUserInfoUri", authUrl + "/userinfo");
formTester.setValue(prefix + "displayOnOidc:oidcJwkSetUri", authUrl + "/jws.json");
formTester.setValue(prefix + "displayOnOidc:oidcLogoutUri", authUrl + "/logout");
formTester.setValue(prefix + "displayOnOidc:oidcForceAuthorizationUriHttps", true);
formTester.setValue(prefix + "displayOnOidc:oidcForceTokenUriHttps", true);
formTester.setValue(prefix + "displayOnOidc:oidcEnforceTokenValidation", true);
formTester.setValue(prefix + "displayOnOidc:oidcUsePKCE", true);
formTester.setValue(prefix + "displayOnOidc:oidcAllowUnSecureLogging", true);
formTester.setValue(prefix + "displayOnOidc:oidcResponseMode", "query");
formTester.setValue(prefix + "displayOnOidc:oidcAuthenticationMethodPostSecret", true);
// when: save
clickSave();
// then: no error
tester.assertNoErrorMessage();
// when: open edit
clickNamedServiceConfig(filterName);
// then: assert all values present in configuration
newFormTester("panel:panel:form");
Component lPanel = formTester.getForm().get("panel");
assertNotNull(lPanel);
assertEquals(OAuth2LoginAuthProviderPanel.class, lPanel.getClass());
OAuth2LoginAuthProviderPanel lOauthPanel = (OAuth2LoginAuthProviderPanel) lPanel;
GeoServerOAuth2LoginFilterConfig lConfig = lOauthPanel.getConfigModel().getObject();
// common
assertEquals("https://localhost:9090/geoserver", lConfig.getBaseRedirectUri());
assertEquals("https://localhost:9090/geoserver/postlogout", lConfig.getPostLogoutRedirectUri());
assertEquals(Boolean.FALSE, lConfig.getEnableRedirectAuthenticationEntryPoint());
// Google
assertEquals(Boolean.TRUE, lConfig.isGoogleEnabled());
assertEquals("googleClientId", lConfig.getGoogleClientId());
assertEquals("googleClientSecret", lConfig.getGoogleClientSecret());
assertEquals("googleUserNameAttribute", lConfig.getGoogleUserNameAttribute());
assertEquals("https://localhost:9090/geoserver/login/oauth2/code/google", lConfig.getGoogleRedirectUri());
// gitHub
assertEquals(Boolean.TRUE, lConfig.isGitHubEnabled());
assertEquals("gitHubClientId", lConfig.getGitHubClientId());
assertEquals("gitHubClientSecret", lConfig.getGitHubClientSecret());
assertEquals("gitHubUserNameAttribute", lConfig.getGitHubUserNameAttribute());
assertEquals("https://localhost:9090/geoserver/login/oauth2/code/gitHub", lConfig.getGitHubRedirectUri());
// MS
assertEquals(Boolean.TRUE, lConfig.isMsEnabled());
assertEquals("msClientId", lConfig.getMsClientId());
assertEquals("msClientSecret", lConfig.getMsClientSecret());
assertEquals("msUserNameAttribute", lConfig.getMsUserNameAttribute());
assertEquals("msScopes", lConfig.getMsScopes());
assertEquals("https://localhost:9090/geoserver/login/oauth2/code/microsoft", lConfig.getMsRedirectUri());
// OIDC
assertEquals(Boolean.TRUE, lConfig.isOidcEnabled());
assertEquals("oidcClientId", lConfig.getOidcClientId());
assertEquals("oidcClientSecret", lConfig.getOidcClientSecret());
assertEquals("oidcUserNameAttribute", lConfig.getOidcUserNameAttribute());
assertEquals("oidcScopes", lConfig.getOidcScopes());
assertEquals("https://localhost:9090/geoserver/login/oauth2/code/oidc", lConfig.getOidcRedirectUri());
assertTrue(lConfig.getOidcForceAuthorizationUriHttps());
assertTrue(lConfig.isOidcEnforceTokenValidation());
assertTrue(lConfig.isOidcUsePKCE());
assertTrue(lConfig.isOidcAllowUnSecureLogging());
assertEquals("query", lConfig.getOidcResponseMode());
assertTrue(lConfig.isOidcAuthenticationMethodPostSecret());
tester.assertModelValue("panel:panel:form:panel:pfv:4:settings:displayOnOidc:oidcResponseMode", "query");
// when: some values changed in edit mode
prefix = "panel:pfv:4:settings:";
formTester.setValue(prefix + "displayOnOidc:oidcForceAuthorizationUriHttps", false);
formTester.setValue(prefix + "displayOnOidc:oidcEnforceTokenValidation", false);
formTester.setValue(prefix + "displayOnOidc:oidcUsePKCE", false);
formTester.setValue(prefix + "displayOnOidc:oidcAllowUnSecureLogging", false);
formTester.setValue(prefix + "displayOnOidc:oidcResponseMode", "");
formTester.setValue(prefix + "displayOnOidc:oidcAuthenticationMethodPostSecret", false);
// when: saved
clickSave();
// then: no error
tester.assertNoErrorMessage();
clickNamedServiceConfig(filterName);
// then: in edit mode all modified values must be present
newFormTester("panel:panel:form");
lPanel = formTester.getForm().get("panel");
assertNotNull(lPanel);
assertEquals(OAuth2LoginAuthProviderPanel.class, lPanel.getClass());
lOauthPanel = (OAuth2LoginAuthProviderPanel) lPanel;
lConfig = lOauthPanel.getConfigModel().getObject();
assertFalse(lConfig.getOidcForceAuthorizationUriHttps());
assertFalse(lConfig.isOidcEnforceTokenValidation());
assertFalse(lConfig.isOidcUsePKCE());
assertFalse(lConfig.isOidcAllowUnSecureLogging());
assertNull(lConfig.getOidcResponseMode());
assertFalse(lConfig.isOidcAuthenticationMethodPostSecret());
}
private void setBasicProviderValues(String pPrefix, String pValuePrefix) {
String enableComponentPath = pPrefix + "enabled";
formTester.setValue(enableComponentPath, true);
pPrefix = pPrefix + "settings:";
formTester.setValue(pPrefix + "clientId", pValuePrefix + "ClientId");
formTester.setValue(pPrefix + "clientSecret", pValuePrefix + "ClientSecret");
formTester.setValue(pPrefix + "userNameAttribute", pValuePrefix + "UserNameAttribute");
}
@Override
protected AbstractSecurityPage getBasePage() {
return new AuthenticationPage();
}
@Override
protected String getBasePanelId() {
return "form:authFilters";
}
@Override
protected Integer getTabIndex() {
return 2;
}
@Override
protected Class<? extends Component> getNamedServicesClass() {
return OAuth2LoginAuthProviderPanel.class;
}
@Override
protected String getDetailsFormComponentId() {
return "authenticationFilterPanel:namedConfig";
}
protected void navigateToOpenIdPanel(String name) throws Exception {
initializeForXML();
activatePanel();
// Test simple add
clickAddNew();
tester.assertRenderedPage(SecurityNamedServiceNewPage.class);
setSecurityConfigClassName(OAuth2LoginAuthProviderPanelInfo.class);
newFormTester();
setSecurityConfigName(name);
}
}

View File

@ -0,0 +1,100 @@
package org.geoserver.web.security.oauth2;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.logging.Level;
import org.geoserver.data.test.SystemTestData;
import org.geoserver.security.GeoServerSecurityFilterChain;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.RequestFilterChain;
import org.geoserver.security.config.SecurityManagerConfig;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.geoserver.security.validation.SecurityConfigException;
import org.geoserver.test.TestSetup;
import org.geoserver.test.TestSetupFrequency;
import org.geoserver.web.GeoServerHomePage;
import org.geoserver.web.GeoServerWicketTestSupport;
import org.junit.BeforeClass;
import org.junit.Test;
@TestSetup(run = TestSetupFrequency.REPEAT)
public class OpenIdConnectLoginButtonTest extends GeoServerWicketTestSupport {
private static final String MARKUP_IMG =
"<img src=\"./wicket/resource/org.geoserver.web.security.oauth2.login.OAuth2LoginAuthProviderPanel/openid";
private static final String MARKUP_FORM =
"<form class=\"d-inline-block\" method=\"GET\" action=\"http://localhost/context/oauth2/authorization/oidc\">";
@Override
protected String getLogConfiguration() {
return "DEFAULT_LOGGING";
}
@Override
protected void onSetUp(SystemTestData testData) throws Exception {
super.onSetUp(testData);
}
private void activateOidcFilterWithEnabledState(boolean pEnabled)
throws IOException, SecurityConfigException, Exception {
GeoServerSecurityManager manager = getSecurityManager();
GeoServerOAuth2LoginFilterConfig filterConfig = new GeoServerOAuth2LoginFilterConfig();
filterConfig.setName("openidconnect");
filterConfig.setClassName(GeoServerOAuth2LoginAuthenticationFilter.class.getName());
filterConfig.setOidcEnabled(pEnabled);
filterConfig.setOidcClientId("foo");
filterConfig.setOidcClientSecret("bar");
filterConfig.setOidcTokenUri("https://www.connectid/fake/test");
filterConfig.setOidcAuthorizationUri("https://www.connectid/fake/test");
filterConfig.setOidcUserInfoUri("https://www.connectid/fake/test");
filterConfig.setOidcJwkSetUri("https://www.connectid/fake/test");
manager.saveFilter(filterConfig);
SecurityManagerConfig config = manager.getSecurityConfig();
GeoServerSecurityFilterChain chain = config.getFilterChain();
RequestFilterChain www = chain.getRequestChainByName("web");
www.setFilterNames("openidconnect", "anonymous");
manager.saveSecurityConfig(config);
}
@Override
protected void setUpTestData(SystemTestData testData) throws Exception {
// no test data to setup, this is a smoke test
}
@BeforeClass
public static void setup() {
System.setProperty(GeoServerOAuth2LoginFilterConfig.OPENID_TEST_GS_PROXY_BASE, "http://localhost/geoserver");
}
@Test
public void testLoginButtonPresentWithOidcEnabled() throws SecurityConfigException, IOException, Exception {
boolean lOidcEnabled = true;
activateOidcFilterWithEnabledState(lOidcEnabled);
tester.startPage(GeoServerHomePage.class);
String html = tester.getLastResponseAsString();
LOGGER.log(Level.INFO, "Last HTML page output:\n" + html);
// the login form is there and has the link
assertTrue(html.contains(MARKUP_FORM));
assertTrue(html.contains(MARKUP_IMG));
}
@Test
public void testLoginButtonOmittedWithOidcDisabled() throws SecurityConfigException, IOException, Exception {
boolean lOidcEnabled = false;
activateOidcFilterWithEnabledState(lOidcEnabled);
tester.startPage(GeoServerHomePage.class);
String html = tester.getLastResponseAsString();
LOGGER.log(Level.INFO, "Last HTML page output:\n" + html);
// the login form is there and has the link
assertFalse(html.contains(MARKUP_FORM));
assertFalse(html.contains(MARKUP_IMG));
}
}

View File

@ -0,0 +1,62 @@
/* (c) 2020 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.security.oauth2.login;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.Map;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import org.apache.commons.io.IOUtils;
import org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.web.client.RestTemplate;
@RunWith(MockitoJUnitRunner.class)
public class DiscoveryClientTest {
@Mock
RestTemplate restTemplate;
JSONObject discovery;
@Before
public void setupDiscovery() throws IOException {
System.setProperty(GeoServerOAuth2LoginFilterConfig.OPENID_TEST_GS_PROXY_BASE, "http://localhost/geoserver");
String json = IOUtils.toString(getClass().getResourceAsStream("discovery.json"), "UTF-8");
this.discovery = (JSONObject) JSONSerializer.toJSON(json);
}
@Test
public void testServerURL() throws Exception {
testDiscoveryClient("https://server.example.com");
}
@Test
public void testFullURL() throws Exception {
testDiscoveryClient("https://server.example.com/.well-known/openid-configuration");
}
private void testDiscoveryClient(String location) {
DiscoveryClient client = new DiscoveryClient(location, restTemplate);
Mockito.when(restTemplate.getForObject(
"https://server.example.com/.well-known/openid-configuration", Map.class))
.thenReturn(discovery);
GeoServerOAuth2LoginFilterConfig config = new GeoServerOAuth2LoginFilterConfig();
client.autofill(config);
assertEquals("https://server.example.com/connect/userinfo", config.getOidcUserInfoUri());
assertEquals("https://server.example.com/jwks.json", config.getOidcJwkSetUri());
assertEquals("https://server.example.com/connect/authorize", config.getOidcAuthorizationUri());
assertEquals("https://server.example.com/connect/token", config.getOidcTokenUri());
assertEquals("openid profile email address phone offline_access", config.getOidcScopes());
}
}

View File

@ -0,0 +1,106 @@
{
"issuer": "https://server.example.com",
"authorization_endpoint": "https://server.example.com/connect/authorize",
"token_endpoint": "https://server.example.com/connect/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"private_key_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256",
"ES256"
],
"userinfo_endpoint": "https://server.example.com/connect/userinfo",
"check_session_iframe": "https://server.example.com/connect/check_session",
"end_session_endpoint": "https://server.example.com/connect/end_session",
"jwks_uri": "https://server.example.com/jwks.json",
"registration_endpoint": "https://server.example.com/connect/register",
"scopes_supported": [
"openid",
"profile",
"email",
"address",
"phone",
"offline_access"
],
"response_types_supported": [
"code",
"code id_token",
"id_token",
"token id_token"
],
"acr_values_supported": [
"urn:mace:incommon:iap:silver",
"urn:mace:incommon:iap:bronze"
],
"subject_types_supported": [
"public",
"pairwise"
],
"userinfo_signing_alg_values_supported": [
"RS256",
"ES256",
"HS256"
],
"userinfo_encryption_alg_values_supported": [
"RSA1_5",
"A128KW"
],
"userinfo_encryption_enc_values_supported": [
"A128CBC-HS256",
"A128GCM"
],
"id_token_signing_alg_values_supported": [
"RS256",
"ES256",
"HS256"
],
"id_token_encryption_alg_values_supported": [
"RSA1_5",
"A128KW"
],
"id_token_encryption_enc_values_supported": [
"A128CBC-HS256",
"A128GCM"
],
"request_object_signing_alg_values_supported": [
"none",
"RS256",
"ES256"
],
"display_values_supported": [
"page",
"popup"
],
"claim_types_supported": [
"normal",
"distributed"
],
"claims_supported": [
"sub",
"iss",
"auth_time",
"acr",
"name",
"given_name",
"family_name",
"nickname",
"profile",
"picture",
"website",
"email",
"email_verified",
"locale",
"zoneinfo",
"http://example.info/claims/groups"
],
"claims_parameter_supported": true,
"service_documentation": "http://server.example.com/connect/service_documentation.html",
"ui_locales_supported": [
"en-US",
"en-GB",
"en-CA",
"fr-FR",
"fr-CA"
]
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ (c) 2018 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.geoserver.community</groupId>
<artifactId>gs-security</artifactId>
<version>2.28-SNAPSHOT</version>
</parent>
<groupId>org.geoserver.community.security</groupId>
<artifactId>gs-sec-oidc</artifactId>
<packaging>pom</packaging>
<name>GeoServer OpenID Connect Security Module</name>
<modules>
<module>oidc-core</module>
<module>oidc-web</module>
<module>oidc-assembly</module>
</modules>
</project>

View File

@ -64,6 +64,13 @@
</modules>
</profile>
<profile>
<id>oidc</id>
<modules>
<module>oidc</module>
</modules>
</profile>
<profile>
<id>oauth2-all</id>
<modules>
@ -85,6 +92,7 @@
<module>oauth2-geonode</module>
<module>oauth2-openid-connect</module>
<module>keycloak</module>
<module>oidc</module>
</modules>
</profile>

View File

@ -7,33 +7,26 @@
package org.geoserver.security.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.security.GeoServerRoleConverter;
import org.geoserver.security.GeoServerRoleService;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.GeoServerUserGroupService;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.config.SecurityNamedServiceConfig;
import org.geoserver.security.filter.GeoServerRoleResolvers.ResolverParam;
import org.geoserver.security.filter.GeoServerRoleResolvers.RoleResolver;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.impl.GeoServerUser;
import org.geoserver.security.impl.RoleCalculator;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.StringUtils;
/**
* J2EE Authentication Filter
*
* @author mcr
*/
public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerPreAuthenticationFilter {
public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerPreAuthenticationFilter
implements GeoServerRoleResolvers.ResolverContext {
private RoleSource roleSource;
private String rolesHeaderAttribute;
@ -45,6 +38,7 @@ public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerP
protected static final String UserNameAlreadyRetrieved = "org.geoserver.security.filter.usernameAlreadyRetrieved";
protected static final String UserName = "org.geoserver.security.filter.username";
@Override
public RoleSource getRoleSource() {
return roleSource;
}
@ -53,6 +47,7 @@ public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerP
this.roleSource = roleSource;
}
@Override
public String getRolesHeaderAttribute() {
return rolesHeaderAttribute;
}
@ -61,6 +56,7 @@ public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerP
this.rolesHeaderAttribute = rolesHeaderAttribute;
}
@Override
public String getUserGroupServiceName() {
return userGroupServiceName;
}
@ -77,6 +73,7 @@ public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerP
this.roleConverterName = roleConverterName;
}
@Override
public String getRoleServiceName() {
return roleServiceName;
}
@ -97,12 +94,10 @@ public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerP
roleConverterName = authConfig.getRoleConverterName();
roleServiceName = authConfig.getRoleServiceName();
// TODO, Justin, is this ok ?
if (PreAuthenticatedUserNameRoleSource.Header.equals(getRoleSource())) {
String converterName = authConfig.getRoleConverterName();
if (converterName == null || converterName.isEmpty())
setConverter(GeoServerExtensions.bean(GeoServerRoleConverter.class));
else setConverter((GeoServerRoleConverter) GeoServerExtensions.bean(converterName));
GeoServerRoleConverter lConverter = GeoServerRoleResolvers.loadConverter(converterName);
setConverter(lConverter);
}
}
@ -137,91 +132,8 @@ public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerP
@Override
protected Collection<GeoServerRole> getRoles(HttpServletRequest request, String principal) throws IOException {
Collection<GeoServerRole> roles;
RoleSource rs = getRoleSource();
if (PreAuthenticatedUserNameRoleSource.RoleService.equals(rs)) {
roles = getRolesFromRoleService(request, principal);
} else if (PreAuthenticatedUserNameRoleSource.UserGroupService.equals(rs)) {
roles = getRolesFromUserGroupService(request, principal);
} else if (PreAuthenticatedUserNameRoleSource.Header.equals(rs)) {
roles = getRolesFromHttpAttribute(request, principal);
} else {
throw new RuntimeException("Couldn't determine roles based on the specified role source [" + rs + "].");
}
LOGGER.log(Level.FINE, "Got roles {0} from {1} for principal {2}", new Object[] {roles, rs, principal});
return roles;
}
/**
* Calculates roles from a {@link GeoServerRoleService} The default service is
* {@link GeoServerSecurityManager#getActiveRoleService()}
*
* <p>The result contains all inherited roles, but no personalized roles
*/
protected Collection<GeoServerRole> getRolesFromRoleService(HttpServletRequest request, String principal)
throws IOException {
boolean useActiveService =
getRoleServiceName() == null || getRoleServiceName().trim().isEmpty();
GeoServerRoleService service = useActiveService
? getSecurityManager().getActiveRoleService()
: getSecurityManager().loadRoleService(getRoleServiceName());
RoleCalculator calc = new RoleCalculator(service);
return calc.calculateRoles(principal);
}
/**
* Calculates roles using a {@link GeoServerUserGroupService} if the principal is not found, an empty collection is
* returned
*/
protected Collection<GeoServerRole> getRolesFromUserGroupService(HttpServletRequest request, String principal)
throws IOException {
Collection<GeoServerRole> roles = new ArrayList<>();
GeoServerUserGroupService service = getSecurityManager().loadUserGroupService(getUserGroupServiceName());
UserDetails details = null;
try {
details = service.loadUserByUsername(principal);
} catch (UsernameNotFoundException ex) {
LOGGER.log(Level.WARNING, "User " + principal + " not found in " + getUserGroupServiceName());
}
if (details != null) {
for (GrantedAuthority auth : details.getAuthorities()) roles.add((GeoServerRole) auth);
}
return roles;
}
/**
* Calculates roles using the String found in the http header attribute if no role string is found, anempty
* collection is returned
*
* <p>The result contains personalized roles
*/
protected Collection<GeoServerRole> getRolesFromHttpAttribute(HttpServletRequest request, String principal)
throws IOException {
Collection<GeoServerRole> roles = new ArrayList<>();
String rolesString = request.getHeader(getRolesHeaderAttribute());
if (rolesString == null || rolesString.trim().isEmpty()) {
LOGGER.log(Level.WARNING, "No roles in header attribute: " + getRolesHeaderAttribute());
return roles;
}
roles.addAll(getConverter().convertRolesFromString(rolesString, principal));
LOGGER.log(
Level.FINE,
"for principal "
+ principal
+ " found roles "
+ StringUtils.collectionToCommaDelimitedString(roles)
+ " in header "
+ getRolesHeaderAttribute());
RoleResolver lResolver = GeoServerRoleResolvers.PRE_AUTH_ROLE_SOURCE_RESOLVER;
Collection<GeoServerRole> roles = lResolver.convert(new ResolverParam(principal, request, this));
return roles;
}
@ -233,6 +145,7 @@ public abstract class GeoServerPreAuthenticatedUserNameFilter extends GeoServerP
return super.getCacheKey(request);
}
@Override
public GeoServerRoleConverter getConverter() {
return converter;
}

View File

@ -0,0 +1,322 @@
/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.exception.GeoServerRuntimException;
import org.geoserver.security.GeoServerRoleConverter;
import org.geoserver.security.GeoServerRoleService;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.GeoServerUserGroupService;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.impl.RoleCalculator;
import org.geotools.util.logging.Logging;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.StringUtils;
/**
* Provides {@link RoleResolver}s to obtain {@link GeoServerRole}s for a principal name during authentication. Useful
* for filters in pre-authentication scenarios.
*
* <p>To resolve roles provide a {@link ResolverContext} with the contextual information regarding the sources to
* inspect and a {@link ResolverParam} with the current principal name and {@link HttpServletRequest}.
*
* <p>Typically the {@link #PRE_AUTH_ROLE_SOURCE_RESOLVER} should be used which covers the other resolvers.
*/
public class GeoServerRoleResolvers {
private static final Logger LOGGER = Logging.getLogger(GeoServerRoleResolvers.class);
/**
* Loads the {@link GeoServerRoleConverter} for the given name
*
* @param pName optional name
* @return the converter
*/
public static GeoServerRoleConverter loadConverter(String pName) {
GeoServerRoleConverter lConverter;
if (pName == null || pName.isEmpty()) {
lConverter = GeoServerExtensions.bean(GeoServerRoleConverter.class);
} else {
lConverter = (GeoServerRoleConverter) GeoServerExtensions.bean(pName);
}
return lConverter;
}
/**
* Provides the {@link #principal} name and {@link #request} together with the {@link #context} which provides
* access to further information required for the conversion.
*/
public static class ResolverParam {
private final String principal;
private final HttpServletRequest request;
private final ResolverContext context;
/**
* @param pPrincipal
* @param pRequest
* @param pContext
*/
public ResolverParam(String pPrincipal, HttpServletRequest pRequest, ResolverContext pContext) {
super();
principal = pPrincipal;
request = pRequest;
context = pContext;
}
public GeoServerSecurityManager getSecurityManager() {
return context.getSecurityManager();
}
public String getRoleServiceName() {
return context.getRoleServiceName();
}
public String getUserGroupServiceName() {
return context.getUserGroupServiceName();
}
public String getRolesHeaderAttribute() {
return context.getRolesHeaderAttribute();
}
public GeoServerRoleConverter getConverter() {
return context.getConverter();
}
public RoleSource getRoleSource() {
return context.getRoleSource();
}
/** @return the principal */
public String getPrincipal() {
return principal;
}
/** @return the request */
public HttpServletRequest getRequest() {
return request;
}
/** @return the context */
public ResolverContext getContext() {
return context;
}
}
/**
* Provides access to the {@link RoleSource}. It determines which source shall be considered. Also provides the name
* to be used per source.
*/
public interface ResolverContext {
GeoServerSecurityManager getSecurityManager();
String getRoleServiceName();
String getUserGroupServiceName();
String getRolesHeaderAttribute();
GeoServerRoleConverter getConverter();
RoleSource getRoleSource();
}
/** Default implementation of a {@link ResolverContext}. */
public static class DefaultResolverContext implements ResolverContext {
private GeoServerSecurityManager securityManager;
private String roleServiceName;
private String userGroupServiceName;
private String rolesHeaderAttribute;
private GeoServerRoleConverter converter;
private RoleSource roleSource;
/**
* @param pSecurityManager
* @param pRoleServiceName
* @param pUserGroupServiceName
* @param pRolesHeaderAttribute
* @param pConverter
* @param pRoleSource
*/
public DefaultResolverContext(
GeoServerSecurityManager pSecurityManager,
String pRoleServiceName,
String pUserGroupServiceName,
String pRolesHeaderAttribute,
GeoServerRoleConverter pConverter,
RoleSource pRoleSource) {
super();
securityManager = pSecurityManager;
roleServiceName = pRoleServiceName;
userGroupServiceName = pUserGroupServiceName;
rolesHeaderAttribute = pRolesHeaderAttribute;
converter = pConverter;
roleSource = pRoleSource;
}
/** @return the securityManager */
@Override
public GeoServerSecurityManager getSecurityManager() {
return securityManager;
}
/** @return the roleServiceName */
@Override
public String getRoleServiceName() {
return roleServiceName;
}
/** @return the userGroupServiceName */
@Override
public String getUserGroupServiceName() {
return userGroupServiceName;
}
/** @return the rolesHeaderAttribute */
@Override
public String getRolesHeaderAttribute() {
return rolesHeaderAttribute;
}
/** @return the converter */
@Override
public GeoServerRoleConverter getConverter() {
return converter;
}
/** @return the roleSource */
@Override
public RoleSource getRoleSource() {
return roleSource;
}
}
/** Contract for resolving the {@link GeoServerRole} for a principal. */
public interface RoleResolver extends Converter<ResolverParam, Collection<GeoServerRole>> {}
/**
* Calculates roles from a {@link GeoServerRoleService} The default service is
* {@link GeoServerSecurityManager#getActiveRoleService()}
*
* <p>The result contains all inherited roles, but no personalized roles
*/
public static final RoleResolver ROLE_SERVICE_RESOLVER = p -> {
boolean useActiveService =
p.getRoleServiceName() == null || p.getRoleServiceName().trim().isEmpty();
GeoServerRoleService service;
try {
service = useActiveService
? p.getSecurityManager().getActiveRoleService()
: p.getSecurityManager().loadRoleService(p.getRoleServiceName());
RoleCalculator calc = new RoleCalculator(service);
return calc.calculateRoles(p.principal);
} catch (IOException e) {
throw new GeoServerRuntimException(
"Failed to load roles for user '"
+ p.principal
+ "' from roleService '"
+ p.getRoleServiceName()
+ "'.",
e);
}
};
/**
* Calculates roles using a {@link GeoServerUserGroupService} if the principal is not found, an empty collection is
* returned
*/
public static final RoleResolver USER_GROUP_SERVICE_RESOLVER = p -> {
Collection<GeoServerRole> roles = new ArrayList<>();
GeoServerUserGroupService service;
try {
service = p.getSecurityManager().loadUserGroupService(p.getUserGroupServiceName());
} catch (IOException e) {
throw new GeoServerRuntimException(
"Failed to load roles for user '"
+ p.principal
+ "' from userGroupService '"
+ p.getUserGroupServiceName()
+ "'.",
e);
}
UserDetails details = null;
try {
details = service.loadUserByUsername(p.principal);
} catch (UsernameNotFoundException ex) {
LOGGER.log(Level.WARNING, "User " + p.principal + " not found in " + p.getUserGroupServiceName());
}
if (details != null) {
for (GrantedAuthority auth : details.getAuthorities()) roles.add((GeoServerRole) auth);
}
return roles;
};
/**
* Calculates roles using the String found in the http header attribute if no role string is found, an empty
* collection is returned
*
* <p>The result contains personalized roles
*/
public static final RoleResolver HTTP_HEADER_RESOLVER = p -> {
if (p.getRequest() == null) {
throw new GeoServerRuntimException("Resolving roles from HTTP headers failed. Request not available.");
}
Collection<GeoServerRole> roles = new ArrayList<>();
String rolesString = p.getRequest().getHeader(p.getRolesHeaderAttribute());
if (rolesString == null || rolesString.trim().isEmpty()) {
LOGGER.log(Level.WARNING, "No roles in header attribute: " + p.getRolesHeaderAttribute());
return roles;
}
roles.addAll(p.getConverter().convertRolesFromString(rolesString, p.principal));
LOGGER.log(
Level.FINE,
"for principal "
+ p.principal
+ " found roles "
+ StringUtils.collectionToCommaDelimitedString(roles)
+ " in header "
+ p.getRolesHeaderAttribute());
return roles;
};
/** Resolves {@link GeoServerRole}s when the {@link RoleSource} is a {@link PreAuthenticatedUserNameRoleSource}. */
public static final RoleResolver PRE_AUTH_ROLE_SOURCE_RESOLVER = p -> {
Collection<GeoServerRole> roles;
RoleSource rs = p.getRoleSource();
if (PreAuthenticatedUserNameRoleSource.RoleService.equals(rs)) {
roles = ROLE_SERVICE_RESOLVER.convert(p);
} else if (PreAuthenticatedUserNameRoleSource.UserGroupService.equals(rs)) {
roles = USER_GROUP_SERVICE_RESOLVER.convert(p);
} else if (PreAuthenticatedUserNameRoleSource.Header.equals(rs)) {
roles = HTTP_HEADER_RESOLVER.convert(p);
} else {
String lMsg = "Couldn't determine roles based on the specified role source %s.";
throw new RuntimeException(String.format(lMsg, rs));
}
String lMsg = "Got roles {0} from {1} for principal {2}";
LOGGER.log(Level.FINE, lMsg, new Object[] {roles, rs, p.principal});
return roles;
};
}

View File

@ -104,4 +104,9 @@ public abstract class GeoServerSecurityFilter extends AbstractGeoServerSecurityS
return url;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " [beanName=" + beanName + "]";
}
}

View File

@ -0,0 +1,105 @@
package org.geoserver.security.filter;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.TreeSet;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.security.GeoServerRoleConverter;
import org.geoserver.security.GeoServerRoleService;
import org.geoserver.security.GeoServerSecurityManager;
import org.geoserver.security.GeoServerUserGroupService;
import org.geoserver.security.config.PreAuthenticatedUserNameFilterConfig;
import org.geoserver.security.config.RoleSource;
import org.geoserver.security.impl.GeoServerRole;
import org.geoserver.security.impl.GeoServerRoleConverterImpl;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.userdetails.UserDetails;
public class GeoServerRoleResolversTest {
public static final String PRINCIPAL_NAME = "test_user";
public static final String ROLE_HEADER_ATTRIBUTE = "roleHeader";
private HttpServletRequest request;
private GeoServerSecurityManager securityManager;
private GeoServerRoleService roleService;
private GeoServerUserGroupService userGroupService;
private GeoServerRoleConverter roleConverter;
private List<GeoServerRole> roles;
private TreeSet<GeoServerRole> sortedRoles;
@Before
public void setUp() {
request = mock();
securityManager = mock();
roleService = mock();
userGroupService = mock();
roleConverter = new GeoServerRoleConverterImpl();
roles = List.of(new GeoServerRole("role1"), new GeoServerRole("role2"), new GeoServerRole("role3"));
sortedRoles = new TreeSet<>();
sortedRoles.addAll(roles);
}
@Test
public void testResolveActiveRoleService() throws IOException {
when(securityManager.getActiveRoleService()).thenReturn(roleService);
when(roleService.getRolesForUser(PRINCIPAL_NAME)).thenReturn(sortedRoles);
RoleSource roleSource = PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource.RoleService;
final GeoServerRoleResolvers.DefaultResolverContext context = new GeoServerRoleResolvers.DefaultResolverContext(
securityManager, null, null, null, roleConverter, roleSource);
GeoServerRoleResolvers.RoleResolver resolver = GeoServerRoleResolvers.PRE_AUTH_ROLE_SOURCE_RESOLVER;
Collection<GeoServerRole> actualRoles =
resolver.convert(new GeoServerRoleResolvers.ResolverParam(PRINCIPAL_NAME, request, context));
assertEquals(sortedRoles, actualRoles);
}
@Test
public void testResolveRoleService() throws IOException {
final String roleServiceName = "roleService";
when(securityManager.loadRoleService(roleServiceName)).thenReturn(roleService);
when(roleService.getRolesForUser(PRINCIPAL_NAME)).thenReturn(sortedRoles);
RoleSource roleSource = PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource.RoleService;
final GeoServerRoleResolvers.DefaultResolverContext context = new GeoServerRoleResolvers.DefaultResolverContext(
securityManager, roleServiceName, null, null, roleConverter, roleSource);
GeoServerRoleResolvers.RoleResolver resolver = GeoServerRoleResolvers.PRE_AUTH_ROLE_SOURCE_RESOLVER;
Collection<GeoServerRole> actualRoles =
resolver.convert(new GeoServerRoleResolvers.ResolverParam(PRINCIPAL_NAME, request, context));
assertEquals(sortedRoles, actualRoles);
}
@Test
public void testResolveUserGroupService() throws IOException {
final String userGroupServiceName = "userGroupService";
UserDetails details = mock();
when(securityManager.loadUserGroupService(userGroupServiceName)).thenReturn(userGroupService);
when(userGroupService.loadUserByUsername(PRINCIPAL_NAME)).thenReturn(details);
doReturn(roles).when(details).getAuthorities();
RoleSource roleSource =
PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource.UserGroupService;
final GeoServerRoleResolvers.DefaultResolverContext context = new GeoServerRoleResolvers.DefaultResolverContext(
securityManager, null, userGroupServiceName, null, roleConverter, roleSource);
GeoServerRoleResolvers.RoleResolver resolver = GeoServerRoleResolvers.PRE_AUTH_ROLE_SOURCE_RESOLVER;
Collection<GeoServerRole> actualRoles =
resolver.convert(new GeoServerRoleResolvers.ResolverParam(PRINCIPAL_NAME, request, context));
assertEquals(roles, actualRoles);
}
@Test
public void testResolveHeaderAttribute() {
when(request.getHeader(ROLE_HEADER_ATTRIBUTE)).thenReturn("role1;role2;role3");
RoleSource roleSource = PreAuthenticatedUserNameFilterConfig.PreAuthenticatedUserNameRoleSource.Header;
final GeoServerRoleResolvers.DefaultResolverContext context = new GeoServerRoleResolvers.DefaultResolverContext(
securityManager, null, null, ROLE_HEADER_ATTRIBUTE, roleConverter, roleSource);
GeoServerRoleResolvers.RoleResolver resolver = GeoServerRoleResolvers.PRE_AUTH_ROLE_SOURCE_RESOLVER;
Collection<GeoServerRole> actualRoles =
resolver.convert(new GeoServerRoleResolvers.ResolverParam("test_user", request, context));
assertEquals(roles, actualRoles);
}
}

View File

@ -169,6 +169,7 @@ public class GeoServerBasePage extends WebPage implements IAjaxIndicatorAware {
protected void onComponentTag(org.apache.wicket.markup.ComponentTag tag) {
String loginPath = getResourcePath(info.getLoginPath());
tag.put("action", loginPath);
tag.put("method", info.getMethod());
}
};
@ -208,7 +209,7 @@ public class GeoServerBasePage extends WebPage implements IAjaxIndicatorAware {
break;
}
}
loginForm.setVisible(anonymous && filterInChain);
loginForm.setVisible(anonymous && filterInChain && info.isEnabled());
}
});

View File

@ -19,6 +19,8 @@ public class LoginFormInfo extends ComponentInfo<GeoServerBasePage> implements C
private Class<GeoServerSecurityProvider> filterClass;
private String include;
private String loginPath;
private String method = "post";
private boolean enabled = true;
/** Name of the login extension; it will determine also the order displayed for the icons */
public void setName(String name) {
@ -94,9 +96,29 @@ public class LoginFormInfo extends ComponentInfo<GeoServerBasePage> implements C
this.loginPath = loginPath;
}
/** @return the method */
public String getMethod() {
return method;
}
/** @param pMethod the method to set */
public void setMethod(String pMethod) {
method = pMethod;
}
/** Sorts by name the Login extensions */
@Override
public int compareTo(LoginFormInfo other) {
return getName().compareTo(other.getName());
}
/** @return the enabled */
public boolean isEnabled() {
return enabled;
}
/** @param pEnabled the enabled to set */
public void setEnabled(boolean pEnabled) {
enabled = pEnabled;
}
}