mirror of
https://github.com/geoserver/geoserver.git
synced 2025-12-15 16:29:19 +00:00
[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:
parent
b99273fcda
commit
e1f71a6d33
5
.gitignore
vendored
5
.gitignore
vendored
@ -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/
|
||||
|
||||
@ -375,6 +375,12 @@
|
||||
<module>security</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>oidc</id>
|
||||
<modules>
|
||||
<module>security</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>oseo</id>
|
||||
<modules>
|
||||
|
||||
@ -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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":0};" parent="aM9ryv3xv72pqoxQDRHE-1" vertex="1">
|
||||
<mxGeometry x="60" y="160" width="10" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="aM9ryv3xv72pqoxQDRHE-5" value="Authorization-<br>Server" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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<br>-Servlet" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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 &lt;clientRegId&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="<font style="color: rgb(255, 0, 0);">/geoserver/oauth2/authorization/&lt;clientRegId&gt;</font>" 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-<br>AuthenticationFilter" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":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="/&lt;authorizationEndpoint&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="/&lt;login&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="<font style="color: rgb(255, 0, 0);">/geoserver/login/oauth2/code/&lt;clientRegId&gt;</font>" 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="/&lt;tokenEndpoint&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={"edgeStyle":"elbowEdgeStyle","elbow":"vertical","curved":0,"rounded":0};" parent="1" vertex="1">
|
||||
<mxGeometry x="1005" y="638" width="10" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="R4H-Cz9XJfiXXCovYyHO-35" value="/&lt;jwkSetEndpoint&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="/&lt;userInfoEndpoint&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="<u><font style="font-size: 20px;">GeoServer Spring OAuth / OpenID Login Process</font></u><div><span style="font-weight: normal;"><font style="font-size: 12px;">Status 01/2025</font></span></div>" 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<font style="color: rgb(0, 0, 255);"> <b>Authorization Code</b></font>" 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<font style="color: rgb(0, 102, 0);"><b> Access Token / ID Token</b></font>" 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="<font style="color: rgb(0, 102, 0);">Access Token [ + ID Token ]</font>" 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:<br>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:<br>http://localhost:9000/oauth2/authorize?<br>&nbsp;response_type=code&amp;<br>&nbsp;client_id=geoserver&amp;<br>&nbsp;scope=openid&amp;<div>&nbsp;state=WqjM[...]%3D&amp;<br>&nbsp;redirect_uri=http://localhost:8080/geoserver/login/oauth2/code/oidc&amp;<br>&nbsp;nonce=HaS0is[...]</div>" 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:<br>http://localhost:8080/geoserver/login/oauth2/code/oidc? <br>&nbsp;code=gNR1OMrUaQtBSMozB7cSHWTAAUscBS[...]j&amp;<br>&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 <br>* no ID Token was obtained (provider does not support OpenID Connect, currently GitHub) or <br>* call is required due to involved scopes (see OidcUserService)<br>* 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 |
29
src/community/security/oidc/doc/diagrams/readme.md
Normal file
29
src/community/security/oidc/doc/diagrams/readme.md
Normal 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
|
||||
114
src/community/security/oidc/migration-readme.md
Normal file
114
src/community/security/oidc/migration-readme.md
Normal 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 Spring’s 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 Andrea’s 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. It’s 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.
|
||||
38
src/community/security/oidc/oidc-assembly/pom.xml
Normal file
38
src/community/security/oidc/oidc-assembly/pom.xml
Normal 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>
|
||||
@ -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>
|
||||
139
src/community/security/oidc/oidc-core/pom.xml
Normal file
139
src/community/security/oidc/oidc-core/pom.xml
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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> {}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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")));
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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}"
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"access_token": "CPURR33RUz-gGhjwODTd9zXo5JkQx4wS",
|
||||
"id_token": "${id_token}",
|
||||
"scope": "openid profile email phone address",
|
||||
"expires_in": 86400,
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
@ -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=="
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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/
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
pdZfJzYixFM2378U-Tdwm_sLljO1tcSmxRKTZ7utmwBf7zYNMHCA41qsXhyjDdYQzXkkFMvW7gt66Wu-FCyjcThNmUXnoMYaaaC6vQM5xcgZriL6mkDAO1n5LD1chE6uVMOYKuP29LiYIWFy3xOIwPUzqewDCH-9W0IM_tLd-aX6rTidPqVMzKZxLsOVV0kcTQudv0DUiQ0R_6xnovvBdaAgoNm-2QjCBfMBXMEaESQPyRy65cXyQ7DCFSLSbpzJuBSJCVJI7gbuHgwq1pkiFo-dlmwssw9V3_8JdOhqZATK1yjjfyWgm56YtzbPrt5Mz4W1xTygfkMMpOr_SjFXxQ
|
||||
@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVX1U3eXUxTjh1Sk9XRVg0UWhUM3FhekNjTm5HTldYUzNrZlJKLVRkeThNIn0.eyJleHAiOjE3MDA2NzYyODcsImlhdCI6MTcwMDYxNjI4NywiYXV0aF90aW1lIjoxNzAwNjE1MzM3LCJqdGkiOiI1ZGI2OWViNy0yMGFkLTQ5M2ItYjE2YS1mNjEyYzRiNjA5N2MiLCJpc3MiOiJodHRwczovL2xvZ2luLWxpdmUtZGV2Lmdlb2NhdC5saXZlL3JlYWxtcy9kYXZlLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU3NDJhOGY2LTM5NzItNGQ2YS05OGYzLTFjZmU2MDc1ZjE0ZSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImxpdmUta2V5Iiwic2Vzc2lvbl9zdGF0ZSI6IjJkOWNlMDBjLWQ4ZTAtNDk0Ny1iODc4LTk2ZDlhZDYwYWU0OCIsImFjciI6IjAiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJkZWZhdWx0LXJvbGVzLWRhdmUtcmVhbG0iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImxpdmUta2V5Ijp7InJvbGVzIjpbIkdlb25ldHdvcmtBZG1pbmlzdHJhdG9yIiwiR2Vvc2VydmVyQWRtaW5pc3RyYXRvciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSBwaG9uZSBvZmZsaW5lX2FjY2VzcyBtaWNyb3Byb2ZpbGUtand0IGFkZHJlc3MiLCJzaWQiOiIyZDljZTAwYy1kOGUwLTQ5NDctYjg3OC05NmQ5YWQ2MGFlNDgiLCJ1cG4iOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYWRkcmVzcyI6e30sIm5hbWUiOiJkYXZpZCBibGFzYnkiLCJncm91cHMiOlsib2ZmbGluZV9hY2Nlc3MiLCJkZWZhdWx0LXJvbGVzLWRhdmUtcmVhbG0iLCJ1bWFfYXV0aG9yaXphdGlvbiJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCIsImdpdmVuX25hbWUiOiJkYXZpZCIsImZhbWlseV9uYW1lIjoiYmxhc2J5IiwiZW1haWwiOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCJ9.G7Ku40V9B1YcesTFbf9BeTkHlfpnSOmADByQ7AHMamV49K2yFzbRECSsTFH1Ijv_6lofWMnRWM8LUCQQH3HA2V6Y9WiYdfAiV52K9WQr38MDNIZXmpopCVZ9ML21EQnhojsbrDW5JkSQPtwnvwMW7OxFECQo8L4_eU8w6ShWVNwEP0JGPHPI3XCMOn-5Cicj6CHacMvhh1iaufGMOd7Dm8IMNZCtlanqoGLy3N3272n8SuydwN6uL0oD8pauYryY2VfCmSbciLNKy-B7NCkdxtF99vzV7J4y3yAac87V3tZmbO47x-X4DClhxlJ-Pr3p3R3VFW6xhCTd7UgOITVacQ
|
||||
85
src/community/security/oidc/oidc-web/pom.xml
Normal file
85
src/community/security/oidc/oidc-web/pom.xml
Normal 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>
|
||||
@ -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(" "));
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 |
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
29
src/community/security/oidc/pom.xml
Normal file
29
src/community/security/oidc/pom.xml
Normal 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>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
@ -104,4 +104,9 @@ public abstract class GeoServerSecurityFilter extends AbstractGeoServerSecurityS
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.getClass().getSimpleName() + " [beanName=" + beanName + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user