Improving REST v2 API
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/GrantType.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/GrantType.java
index c9058d1..200dc75 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/GrantType.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/GrantType.java
@@ -25,12 +25,19 @@
 {
     @XmlEnumValue( "refresh_token" )
     REFRESH_TOKEN("refresh_token"),
+
     @XmlEnumValue( "authorization_code" )
-    AUTHORIZATION_CODE("authorization_code");
+    AUTHORIZATION_CODE("authorization_code"),
 
-    private String label;
+    @XmlEnumValue( "none" )
+    NONE("none");
 
-    GrantType(String label) {
+    private final String label;
+
+    GrantType(final String label) {
+        if (label==null) {
+            throw new NullPointerException( "Label must not be null" );
+        }
         this.label = label;
     }
 
@@ -40,10 +47,19 @@
 
     public static GrantType byLabel(String label) {
         for (GrantType value : values()) {
-            if (value.equals( label )) {
+            if (value.getLabel().equals( label )) {
                 return value;
             }
         }
         throw new IllegalArgumentException( "Label does not exist " + label );
     }
+
+    @Override
+    public String toString( )
+    {
+        final StringBuilder sb = new StringBuilder( "GrantType{" );
+        sb.append( "label='" ).append( label ).append( '\'' );
+        sb.append( '}' );
+        return sb.toString( );
+    }
 }
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenRefreshRequest.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenRefreshRequest.java
new file mode 100644
index 0000000..a474303
--- /dev/null
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenRefreshRequest.java
@@ -0,0 +1,85 @@
+package org.apache.archiva.redback.rest.api.model.v2;/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.archiva.redback.rest.api.model.GrantType;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.Serializable;
+
+/**
+ * @author Martin Stockhammer <martin_s@apache.org>
+ */
+@XmlRootElement( name = "refreshToken" )
+@Schema( name = "TokenRefreshRequest", description = "Information for requesting token from a refresh token" )
+public class TokenRefreshRequest implements Serializable
+{
+
+    private static final long serialVersionUID = 3900011211040344882L;
+    GrantType grantType;
+    String refreshToken;
+    String scope;
+
+    public TokenRefreshRequest( )
+    {
+    }
+
+    public TokenRefreshRequest( GrantType grantType, String refreshToken, String scope )
+    {
+        this.grantType = grantType;
+        this.refreshToken = refreshToken;
+        this.scope = scope;
+    }
+
+    @XmlElement( name = "grant_type", required = true)
+    @Schema(description = "The grant type for requesting the token. 'refresh_token' for token refresh")
+    public GrantType getGrantType( )
+    {
+        return grantType;
+    }
+
+    public void setGrantType( GrantType grantType )
+    {
+        this.grantType = grantType;
+    }
+
+    @XmlElement( name = "refresh_token" )
+    @Schema(description = "The refresh token that is validated before generating the new access token")
+    public String getRefreshToken( )
+    {
+        return refreshToken;
+    }
+
+    public void setRefreshToken( String refreshToken )
+    {
+        this.refreshToken = refreshToken;
+    }
+
+    @XmlElement( name = "scope")
+    @Schema(description = "The scope for the new access token.")
+    public String getScope( )
+    {
+        return scope;
+    }
+
+    public void setScope( String scope )
+    {
+        this.scope = scope;
+    }
+}
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenRequest.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenRequest.java
index a39d4fc..73f6ad9 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenRequest.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenRequest.java
@@ -32,8 +32,8 @@
 @Schema(name="Request Token Data", description = "Schema used for requesting a Bearer token.")
 public class TokenRequest implements Serializable
 {
-    private static final long serialVersionUID = -4803869713444270526L;
-    GrantType grantType = null;
+    private static final long serialVersionUID = -2420082541650525792L;
+    GrantType grantType = GrantType.NONE;
     String clientId;
     String clientSecret;
     String code;
@@ -68,7 +68,12 @@
     }
 
     @XmlElement(name = "grant_type", required = true )
-    @Schema(description = "The grant type. Currently only 'authorization_code' is supported.")
+    @Schema(
+        name = "grant_type",
+        description = "The grant type. Currently only 'authorization_code' is supported.",
+        allowableValues = {"authorization_code","access_token"},
+        defaultValue = "authorization_code",
+        example = "authorization_code")
     public GrantType getGrantType( )
     {
         return grantType;
@@ -80,6 +85,9 @@
     }
 
     @XmlElement(name="client_id", nillable = true)
+    @Schema(
+        name = "client_id",
+        description = "The client identifier.")
     public String getClientId( )
     {
         return clientId;
@@ -91,6 +99,9 @@
     }
 
     @XmlElement(name="client_secret", nillable = true)
+    @Schema(
+        name = "client_secret",
+        description = "The client application secret.")
     public String getClientSecret( )
     {
         return clientSecret;
@@ -113,7 +124,7 @@
     }
 
     @XmlElement(name="user_id", required = true )
-    @Schema(description = "The user identifier.")
+    @Schema(name="user_id", description = "The user identifier.")
     public String getUserId( )
     {
         return userId;
@@ -131,7 +142,6 @@
         return password;
     }
 
-    @XmlElement(name="password", required = true )
     public void setPassword( String password )
     {
         this.password = password;
@@ -149,6 +159,9 @@
     }
 
     @XmlElement(name="redirect_uri" )
+    @Schema(
+        name = "redirect_uri",
+        description = "The URL to redirect to.")
     public String getRedirectUri( )
     {
         return redirectUri;
@@ -170,4 +183,21 @@
         this.state = state;
     }
 
+
+    @Override
+    public String toString( )
+    {
+        final StringBuilder sb = new StringBuilder( "TokenRequest{" );
+        sb.append( "grantType=" ).append( grantType );
+        sb.append( ", clientId='" ).append( clientId ).append( '\'' );
+        sb.append( ", clientSecret='" ).append( clientSecret ).append( '\'' );
+        sb.append( ", code='" ).append( code ).append( '\'' );
+        sb.append( ", scope='" ).append( scope ).append( '\'' );
+        sb.append( ", state='" ).append( state ).append( '\'' );
+        sb.append( ", userId='" ).append( userId ).append( '\'' );
+        sb.append( ", password='*******'" );
+        sb.append( ", redirectUri='" ).append( redirectUri ).append( '\'' );
+        sb.append( '}' );
+        return sb.toString( );
+    }
 }
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenResponse.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenResponse.java
new file mode 100644
index 0000000..92c1d0e
--- /dev/null
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/TokenResponse.java
@@ -0,0 +1,157 @@
+package org.apache.archiva.redback.rest.api.model.v2;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.archiva.redback.authentication.Token;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.Serializable;
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * @author Martin Stockhammer <martin_s@apache.org>
+ */
+@XmlRootElement(name="token")
+@Schema(name="TokenData", description = "The token response data")
+public class TokenResponse implements Serializable
+{
+
+    private static final long serialVersionUID = 2063260311211245209L;
+    String accessToken;
+    String tokenType = "Bearer";
+    long expiresIn;
+    String refreshToken;
+    String scope;
+    String state;
+
+    public TokenResponse( )
+    {
+    }
+
+    public TokenResponse( String accessToken, String tokenType, long expiresIn, String refreshToken, String scope )
+    {
+        this.accessToken = accessToken;
+        this.tokenType = tokenType;
+        this.expiresIn = expiresIn;
+        this.refreshToken = refreshToken;
+        this.scope = scope;
+    }
+
+    public TokenResponse( String accessToken, long expiresIn, String refreshToken, String scope )
+    {
+        this.accessToken = accessToken;
+        this.expiresIn = expiresIn;
+        this.refreshToken = refreshToken;
+        this.scope = scope;
+    }
+
+    public TokenResponse( Token accessToken, Token refreshToken )
+    {
+        this.expiresIn = Duration.between( Instant.now( ), accessToken.getMetadata( ).validBefore( ) ).getSeconds();
+        this.accessToken = accessToken.getData( );
+        this.refreshToken = refreshToken.getData( );
+        this.scope = "";
+    }
+
+    public TokenResponse( Token accessToken, Token refreshToken , String scope, String state)
+    {
+        this.expiresIn = Duration.between( Instant.now( ), accessToken.getMetadata( ).validBefore( ) ).getSeconds();
+        this.accessToken = accessToken.getData( );
+        this.refreshToken = refreshToken.getData( );
+        this.scope = scope;
+        this.state = state;
+    }
+
+    @XmlElement(name="access_token")
+    @Schema(name = "access_token", description = "The access token that may be used as Bearer token in the Authorization header")
+    public String getAccessToken( )
+    {
+        return accessToken;
+    }
+
+    public void setAccessToken( String accessToken )
+    {
+        this.accessToken = accessToken;
+    }
+
+    @XmlElement(name="token_type")
+    @Schema(name="token_type", description = "The type of the token. Currently only Bearer Tokens are supported.")
+    public String getTokenType( )
+    {
+        return tokenType;
+    }
+
+    public void setTokenType( String tokenType )
+    {
+        this.tokenType = tokenType;
+    }
+
+    @XmlElement(name="expires_in")
+    @Schema(name="expires_in", description = "The time in seconds. After this time the token will expire and is not valid for authentication.")
+    public long getExpiresIn( )
+    {
+        return expiresIn;
+    }
+
+    public void setExpiresIn( long expiresIn )
+    {
+        this.expiresIn = expiresIn;
+    }
+
+    @XmlElement(name="refresh_token")
+    @Schema(name="refresh_token", description = "The refresh token, that can be used for getting a new access token.")
+    public String getRefreshToken( )
+    {
+        return refreshToken;
+    }
+
+    public void setRefreshToken( String refreshToken )
+    {
+        this.refreshToken = refreshToken;
+    }
+
+    @Schema(description = "Scope of the token. Currently there are no scopes defined.")
+    public String getScope( )
+    {
+        return scope;
+    }
+
+    public void setScope( String scope )
+    {
+        this.scope = scope;
+    }
+
+    @Schema(description = "The state value will be returned, if a state is provided in the request.")
+    public String getState( )
+    {
+        return state;
+    }
+
+    public void setState( String state )
+    {
+        this.state = state;
+    }
+
+    public boolean hasState() {
+        return state != null && state.length( ) > 0;
+    }
+}
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java
index 6ddbcee..769566b 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java
@@ -25,11 +25,13 @@
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.apache.archiva.redback.authorization.RedbackAuthorization;
 import org.apache.archiva.redback.rest.api.model.v2.PingResult;
+import org.apache.archiva.redback.rest.api.model.v2.TokenRefreshRequest;
 import org.apache.archiva.redback.rest.api.model.v2.TokenRequest;
-import org.apache.archiva.redback.rest.api.model.TokenResponse;
+import org.apache.archiva.redback.rest.api.model.v2.TokenResponse;
 import org.apache.archiva.redback.rest.api.model.User;
 import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
 
+import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
@@ -44,6 +46,7 @@
 @Path( "/auth" )
 @Tag(name = "v2")
 @Tag(name = "v2/Authentication")
+@Consumes( { MediaType.APPLICATION_JSON })
 public interface AuthenticationService
 {
 
@@ -107,7 +110,7 @@
         }
     )
     @SecurityRequirement( name="BearerAuth" )
-    TokenResponse token( org.apache.archiva.redback.rest.api.model.TokenRequest tokenRequest )
+    TokenResponse token( TokenRefreshRequest tokenRequest )
         throws RedbackServiceException;
 
 
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java
index 851f07f..0f42930 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java
@@ -21,6 +21,7 @@
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.headers.Header;
+import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -96,7 +97,7 @@
             @ApiResponse( responseCode = "201",
                 description = "If the group addition was successful",
                 headers = {
-                    @Header( name="Location", description = "The URL of the created mapping")
+                    @Header( name="Location", description = "The URL of the created mapping", schema = @Schema(type="string"))
                 }
             ),
             @ApiResponse( responseCode = "405", description = "Invalid input" )
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java
index d6d1c27..015660c 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java
@@ -20,6 +20,7 @@
  */
 
 import io.swagger.v3.oas.annotations.headers.Header;
+import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -119,13 +120,13 @@
             @ApiResponse( responseCode = "201",
                 description = "If user creation was successful",
                 headers = {
-                    @Header( name="Location", description = "The URL of the created mapping")
+                    @Header( name="Location", description = "The URL of the created mapping", schema = @Schema(type="string"))
                 }
             ),
             @ApiResponse( responseCode = "422", description = "Invalid input" ),
             @ApiResponse( responseCode = "303", description = "The user exists already",
                 headers = {
-                    @Header( name="Location", description = "The URL of existing user")
+                    @Header( name="Location", description = "The URL of existing user", schema = @Schema(type="string"))
                 }
             )
         }
@@ -187,13 +188,13 @@
             @ApiResponse( responseCode = "201",
                 description = "If user creation was successful",
                 headers = {
-                    @Header( name="Location", description = "The URL of the created mapping")
+                    @Header( name="Location", description = "The URL of the created mapping", schema = @Schema(type="string"))
                 }
             ),
             @ApiResponse( responseCode = "422", description = "Invalid input" ),
             @ApiResponse( responseCode = "303", description = "The user exists already",
                 headers = {
-                    @Header( name="Location", description = "The URL of the existing admin user")
+                    @Header( name="Location", description = "The URL of the existing admin user", schema = @Schema(type="string"))
                 }
             )
         }
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java
index 2ec5381..7553cf0 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java
@@ -40,6 +40,8 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
 import javax.ws.rs.container.ResourceInfo;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
@@ -68,7 +70,7 @@
 @Priority( Priorities.PRECHECK )
 public class RequestValidationInterceptor
     extends AbstractInterceptor
-    implements ContainerRequestFilter
+    implements ContainerRequestFilter, ContainerResponseFilter
 {
 
 
@@ -113,6 +115,21 @@
 
     private UserConfiguration config;
 
+    @Override
+    public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext ) throws IOException
+    {
+        responseContext.getHeaders().add(
+            "Access-Control-Allow-Origin", "http://localhost:4200");
+        responseContext.getHeaders().add(
+            "Access-Control-Allow-Credentials", "true");
+        responseContext.getHeaders().add(
+            "Access-Control-Allow-Headers",
+            "origin, content-type, accept, authorization");
+        responseContext.getHeaders().add(
+            "Access-Control-Allow-Methods",
+            "GET, POST, PUT, DELETE, OPTIONS, HEAD");
+    }
+
     private class HeaderValidationInfo
     {
 
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java
index d2d99c5..edc7a4f 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java
@@ -35,6 +35,7 @@
 import org.apache.archiva.redback.rest.api.model.User;
 import org.apache.archiva.redback.rest.api.model.UserLogin;
 import org.apache.archiva.redback.rest.api.model.v2.PingResult;
+import org.apache.archiva.redback.rest.api.model.v2.TokenRefreshRequest;
 import org.apache.archiva.redback.rest.api.model.v2.TokenRequest;
 import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
 import org.apache.archiva.redback.rest.api.services.v2.AuthenticationService;
@@ -125,6 +126,7 @@
     public TokenResponse logIn( TokenRequest loginRequest )
         throws RedbackServiceException
     {
+        log.debug( "Login request: grantType={}, code={}", loginRequest.getGrantType( ), loginRequest.getCode( ) );
         if (!GrantType.AUTHORIZATION_CODE.equals(loginRequest.getGrantType())) {
             throw new RedbackServiceException( ErrorMessage.of( ERR_AUTH_BAD_CODE ), Response.Status.FORBIDDEN.getStatusCode( ) );
         }
@@ -203,7 +205,7 @@
     }
 
     @Override
-    public TokenResponse token( org.apache.archiva.redback.rest.api.model.TokenRequest request ) throws RedbackServiceException
+    public TokenResponse token( TokenRefreshRequest request ) throws RedbackServiceException
     {
         if (!GrantType.REFRESH_TOKEN.equals(request.getGrantType())) {
             log.debug( "Bad grant type {}, expected: refresh_token", request.getGrantType( ).name( ).toLowerCase( ) );