Changeset 983

Show
Ignore:
Timestamp:
22-10-10 16:18:34 (4 years ago)
Author:
robert@…
Message:

New type of authentication and authorization added to the rest controller. See ticket 118

Location:
trunk
Files:
6 added
1 removed
5 modified

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/conf/Config.groovy

    r976 r983  
    164164grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'username' // Use the persons username as salt for encryption 
    165165grails.plugins.springsecurity.securityConfigType = grails.plugins.springsecurity.SecurityConfigType.Annotation 
     166grails.plugins.springsecurity.successHandler.targetUrlParameter = 'spring-security-redirect' 
    166167 
    167168// Make sure the different controllers provided by springsecurity.ui are only accessible by administrators 
  • trunk/grails-app/controllers/dbnp/authentication/LoginController.groovy

    r976 r983  
    2424         */ 
    2525        def springSecurityService 
     26 
     27        /** 
     28         * Dependency injection for the GSCF authentication service 
     29         */ 
     30        def AuthenticationService 
    2631 
    2732        /** 
     
    5560        } 
    5661 
     62        /** 
     63         * Shows the login page for users from a module 
     64         */ 
     65        def auth_remote = { 
     66            def consumer    = params.consumer 
     67            def token       = params.token 
     68 
     69                        if( consumer == null || token == null ) { 
     70                                throw new Exception( "Consumer and Token must be given!" ); 
     71                        } 
     72 
     73            def returnUrl   = params.returnUrl 
     74 
     75            // If the user is already authenticated with this session_id, redirect 
     76            // him 
     77            if( AuthenticationService.isRemotelyLoggedIn( consumer, token ) ) { 
     78                if( returnUrl ) { 
     79                                        redirect url: returnUrl 
     80                } else { 
     81                    redirect controller: 'home' 
     82                } 
     83            } 
     84 
     85            // If the user is already logged in locally, we log him in and 
     86            // immediately redirect him 
     87            if (AuthenticationService.isLoggedIn()) { 
     88                                AuthenticationService.logInRemotely( consumer, token, AuthenticationService.getLoggedInUser() ) 
     89 
     90                                if( returnUrl ) { 
     91                    redirect url: returnUrl 
     92                } else { 
     93                    redirect controller: 'home' 
     94                } 
     95            } 
     96 
     97            // Otherwise we show the login screen 
     98                        def config = SpringSecurityUtils.securityConfig 
     99            String view = 'auth' 
     100            String postUrl = "${request.contextPath}${config.apf.filterProcessesUrl}" 
     101            String redirectUrl = g.createLink( absolute: true, controller: 'login', action: 'auth_remote', params: [ consumer: params.consumer, token: params.token, returnUrl: params.returnUrl ] ) 
     102            render view: view, model: [postUrl: postUrl, 
     103                                       rememberMeParameter: config.rememberMe.parameter, redirectUrl: redirectUrl ] 
     104        } 
     105         
    57106        /** 
    58107         * Show denied page. 
  • trunk/grails-app/controllers/RestController.groovy

    r976 r983  
    3636        /** 
    3737         * Authorization closure, which is run before executing any of the REST resource actions 
    38          * It fetches a username/password combination from basic HTTP authentication and checks whether 
    39          * that is an active (SecuritySpring) account 
    40          * @return 
     38         * It fetches a consumer/token combination from the url and checks whether 
     39         * that is a correct and known combination 
     40         * 
     41         * @param       consumer        consumer name of the calling module 
     42         * @param       token           token for the authenticated user (e.g. session_id) 
     43         * @return true if the user is remotely logged in, false otherwise 
    4144         */ 
    4245        private def auth() { 
    43             credentials = BasicAuthentication.credentialsFromRequest(request)            
    44         requestUser = AuthenticationService.authenticateUser(credentials.u, credentials.p) 
    45                  
    46                 if(!requestUser) { 
    47                     response.sendError(403) 
    48                 return false 
    49             } 
    50                 else { 
     46                if( !AuthenticationService.isRemotelyLoggedIn( params.consumer, params.token ) ) { 
     47                        response.sendError(403) 
     48                        return false 
     49                } else { 
    5150                        return true 
    5251                } 
     
    5453 
    5554        /** 
    56         * REST resource for data modules. 
    57         * Username and password should be supplied via HTTP Basic Authentication. 
    58         * Determines whether the given user/password combination is a valid GSCF account. 
    59         * 
    60         * @return bool {"authenticated":true} when user/password is a valid GSCF account, {"authenticated":false} otherwise. 
    61         */ 
    62         def isUser= { 
    63                 boolean isUser 
    64                 credentials = BasicAuthentication.credentialsFromRequest(request) 
    65                 def reqUser = AuthenticationService.authenticateUser(credentials.u, credentials.p) 
    66                 isUser = reqUser ? true : false 
     55         * REST resource for data modules. 
     56         * Consumer and token should be supplied via URL parameters. 
     57         * Determines whether the given user/password combination is a valid GSCF account. 
     58         * 
     59         * @param       consumer        consumer name of the calling module 
     60         * @param       token           token for the authenticated user (e.g. session_id) 
     61         * @return bool {"authenticated":true} when user/password is a valid GSCF account, {"authenticated":false} otherwise. 
     62         */ 
     63        def isUser = { 
     64                boolean isUser = AuthenticationService.isRemotelyLoggedIn( params.consumer, params.token ) 
    6765                def reply = ['authenticated':isUser] 
    6866                render reply as JSON 
     
    7169 
    7270        /** 
    73         * REST resource for data modules. 
    74         * Username and password should be supplied via HTTP Basic Authentication. 
    75         * Provide a list of all studies owned by the supplied user. 
    76         * 
    77         * @return JSON object list containing 'studyToken', and 'name' (title) for each study 
    78         */ 
     71         * REST resource for data modules. 
     72         * Consumer and token should be supplied via URL parameters. 
     73         * Provide a list of all studies owned by the supplied user. 
     74         * 
     75         * @param       consumer        consumer name of the calling module 
     76         * @param       token           token for the authenticated user (e.g. session_id) 
     77         * @return JSON object list containing 'studyToken', and 'name' (title) for each study 
     78         */ 
    7979        def getStudies = { 
    8080                List studies = []  
    81                 def user = params.user 
    82                 Study.findAllByOwner(requestUser).each { study -> 
     81                Study.findAllByOwner(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token )).each { study -> 
    8382                        studies.push( [ 'title':study.title, 'studyToken':study.getToken()] ) 
    8483                } 
     
    8887 
    8988        /** 
    90         * REST resource for data modules. 
    91         * Username and password should be supplied via HTTP Basic Authentication. 
    92         * Provide a list of all subjects belonging to a study. 
    93         * 
    94         * @param studyToken String The external study id (code) of the target GSCF Study object 
    95         * @return JSON object list of subject names 
    96         */ 
     89         * REST resource for data modules. 
     90         * Consumer and token should be supplied via URL parameters. 
     91         * Provide a list of all subjects belonging to a study. 
     92         * 
     93         * If the user is not allowed to read the study contents, a 401 error is given 
     94         * 
     95         * @param       studyToken      String The external study id (code) of the target GSCF Study object 
     96         * @param       consumer        consumer name of the calling module 
     97         * @param       token           token for the authenticated user (e.g. session_id) 
     98         * @return JSON object list of subject names 
     99         */ 
    97100        def getSubjects = { 
    98101                List subjects = []  
     
    100103                        def id = params.studyToken 
    101104                        def study = Study.find( "from Study as s where s.code=?", [id]) 
    102                         if(study) study.subjects.each { subjects.push it.name } 
     105 
     106                        if(study) { 
     107                                // Check whether the person is allowed to read the data of this study 
     108                                if( !study.canRead(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token ))) { 
     109                                        response.sendError(401) 
     110                                        return false 
     111                                } 
     112 
     113                                study.subjects.each { subjects.push it.name } 
     114                        } 
    103115                } 
    104116                render subjects as JSON  
     
    107119 
    108120        /** 
    109         * REST resource for data modules. 
    110         * Username and password should be supplied via HTTP Basic Authentication. 
    111         * Provide a list of all assays for a given study 
    112         * 
    113         * Example call of the getAssays REST resource:  
    114         * http://localhost:8080/gscf/rest/getAssays?studyToken=PPSH&moduleURL=http://localhost:8182/sam 
    115         * 
    116         * @param stuyToken String The external study id (code) of the target GSCF Study object 
    117         * @param moduleURL String The base URL of the calling dbNP module 
    118         * @return list of assays in the study as JSON object list, filtered to only contain assays  
    119         *         for the specified module, with 'assayToken' and 'name' for each assay 
    120         */ 
     121         * REST resource for data modules. 
     122         * Consumer and token should be supplied via URL parameters. 
     123         * Provide a list of all assays for a given study. 
     124         * 
     125         * If the user is not allowed to read the study contents, a 401 error is given 
     126         * 
     127         * Example call of the getAssays REST resource: 
     128         * http://localhost:8080/gscf/rest/getAssays?studyToken=PPSH&moduleURL=http://localhost:8182/sam 
     129         * 
     130         * @param       studyToken      String The external study id (code) of the target GSCF Study object 
     131         * @param       moduleURL       String The base URL of the calling dbNP module 
     132         * @param       consumer        consumer name of the calling module 
     133         * @param       token           token for the authenticated user (e.g. session_id) 
     134         * @return list of assays in the study as JSON object list, filtered to only contain assays 
     135         *         for the specified module, with 'assayToken' and 'name' for each assay 
     136         */ 
    121137        def getAssays = { 
    122138                List assays = []  
     
    124140                        def id = params.studyToken 
    125141                        def study = Study.find( "from Study as s where s.code=?", [id] ) 
    126                         if(study && study.owner == requestUser) study.assays.each{ assay -> 
    127                                 if (assay.module.url.equals(params.moduleURL)) { 
    128                                 def map = ['name':assay.name, 'assayToken':assay.getToken()] 
    129                                         assays.push( map ) 
     142 
     143                        if(study) { 
     144                                // Check whether the person is allowed to read the data of this study 
     145                                if( !study.canRead(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token ))) { 
     146                                        response.sendError(401) 
     147                                        return false 
     148                                } 
     149 
     150                                study.assays.each{ assay -> 
     151                                        if (assay.module.url.equals(params.moduleURL)) { 
     152                                                def map = ['name':assay.name, 'assayToken':assay.getToken()] 
     153                                                assays.push( map ) 
     154                                        } 
    130155                                } 
    131156                        } 
     
    136161 
    137162        /** 
    138         * REST resource for data modules. 
    139         * Username and password should be supplied via HTTP Basic Authentication. 
    140         * Provide all samples of a given Assay. The result is an enriched list with additional information for each sample. 
    141         * 
    142         * @param assayToken String (assayToken of some Assay in GSCF) 
    143         * @return As a JSON object list, for each sample in that assay: 
    144         * @return 'name' (Sample name, which is unique) 
    145         * @return 'material' (Sample material) 
    146         * @return 'subject' (The name of the subject from which the sample was taken) 
    147         * @return 'event' (the name of the template of the SamplingEvent describing the sampling) 
    148         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string) 
    149         */ 
     163         * REST resource for data modules. 
     164         * Username and password should be supplied via HTTP Basic Authentication. 
     165         * Provide all samples of a given Assay. The result is an enriched list with additional information for each sample. 
     166         * 
     167         * @param       assayToken      String (assayToken of some Assay in GSCF) 
     168         * @param       consumer        consumer name of the calling module 
     169         * @param       token           token for the authenticated user (e.g. session_id) 
     170         * @return As a JSON object list, for each sample in that assay: 
     171         * @return 'name' (Sample name, which is unique) 
     172         * @return 'material' (Sample material) 
     173         * @return 'subject' (The name of the subject from which the sample was taken) 
     174         * @return 'event' (the name of the template of the SamplingEvent describing the sampling) 
     175         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string) 
     176         */ 
    150177        def getSamples = { 
    151178                def items = [] 
     
    170197 
    171198        /** 
    172         * REST resource for dbNP modules. 
    173         * 
    174         * @param studyToken String, the external identifier of the study 
    175         * @return List of all fields of this study 
    176         * @return  
    177         * 
    178         * Example REST call (without authentication):  
    179     * http://localhost:8080/gscf/rest/getStudy/study?studyToken=PPSH 
    180     * 
    181         * Returns the JSON object:  
    182         * {"title":"NuGO PPS human study","studyToken":"PPSH","startDate":"2008-01-13T23:00:00Z", 
    183         * "Description":"Human study performed at RRI; centres involved: RRI, IFR, TUM, Maastricht U.", 
    184         * "Objectives":null,"Consortium":null,"Cohort name":null,"Lab id":null,"Institute":null, 
    185         * "Study protocol":null} 
     199         * REST resource for dbNP modules. 
     200         * 
     201         * @param       studyToken String, the external identifier of the study 
     202         * @param       consumer        consumer name of the calling module 
     203         * @param       token           token for the authenticated user (e.g. session_id) 
     204         * @return List of all fields of this study 
     205         * @return 
     206         * 
     207         * If the user is not allowed to read this study, a 401 error is given 
     208         * 
     209         * Example REST call (without authentication): 
     210     * http://localhost:8080/gscf/rest/getStudy/study?studyToken=PPSH 
     211     * 
     212         * Returns the JSON object: 
     213         * {"title":"NuGO PPS human study","studyToken":"PPSH","startDate":"2008-01-13T23:00:00Z", 
     214         * "Description":"Human study performed at RRI; centres involved: RRI, IFR, TUM, Maastricht U.", 
     215         * "Objectives":null,"Consortium":null,"Cohort name":null,"Lab id":null,"Institute":null, 
     216         * "Study protocol":null} 
    186217        */ 
    187218        def getStudy = { 
     
    190221                        def study = Study.find( "from Study as s where code=?",[params.studyToken]) 
    191222                        if(study) { 
     223                                // Check whether the person is allowed to read the data of this study 
     224                                if( !study.canRead(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token ))) { 
     225                                        response.sendError(401) 
     226                                        return false 
     227                                } 
     228                                 
    192229                                study.giveFields().each { field -> 
    193230                                        def name = field.name 
     
    203240 
    204241        /** 
    205         * REST resource for dbNP modules. 
    206         * 
    207         * @param assayToken String, the external identifier of the study 
    208         * @return List of all fields of this assay  
    209         * 
    210         * Example REST call (without authentication):  
    211     * http://localhost:8080/gscf/rest/getAssay/assay?assayToken=PPS3_SAM 
    212     * 
    213         * Returns the JSON object: {"name":"Lipid profiling","module":{"class":"dbnp.studycapturing.AssayModule","id":1, 
    214         * "name":"SAM module for clinical data","platform":"clinical measurements","url":"http://sam.nmcdsp.org"}, 
    215         * "assayToken":"PPS3_SAM","parentStudyToken":"PPS","Description":null} 
    216         */ 
     242         * REST resource for dbNP modules. 
     243         * 
     244         * @param       assayToken String, the external identifier of the study 
     245         * @param       consumer        consumer name of the calling module 
     246         * @param       token           token for the authenticated user (e.g. session_id) 
     247         * @return List of all fields of this assay 
     248         * 
     249         * Example REST call (without authentication): 
     250     * http://localhost:8080/gscf/rest/getAssay/assay?assayToken=PPS3_SAM 
     251     * 
     252         * Returns the JSON object: {"name":"Lipid profiling","module":{"class":"dbnp.studycapturing.AssayModule","id":1, 
     253         * "name":"SAM module for clinical data","platform":"clinical measurements","url":"http://sam.nmcdsp.org"}, 
     254         * "assayToken":"PPS3_SAM","parentStudyToken":"PPS","Description":null} 
     255         */ 
    217256        def getAssay = { 
    218257                def items = [:] 
     
    234273 
    235274        /** 
    236         * REST resource for data modules. 
    237         * Username and password should be supplied via HTTP Basic Authentication. 
    238         * One specific sample of a given Assay. 
    239         * 
    240         * @param assayToken String (id of some Assay in GSCF) 
    241         * @return As a JSON object list, for each sample in that assay: 
    242         * @return 'name' (Sample name, which is unique) 
    243         * @return 'material' (Sample material) 
    244         * @return 'subject' (The name of the subject from which the sample was taken) 
    245         * @return 'event' (the name of the template of the SamplingEvent describing the sampling) 
    246         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string) 
    247         * 
    248         * Example REST call (without authentication):  
    249     * http://localhost:8080/gscf/rest/getSample/sam?assayToken=PPS3_SAM&sampleToken=A30_B  
    250     * 
    251         * Returns the JSON object:  
    252         * {"subject":"A30","event":"Liver extraction","startTime":"1 week, 1 hour", 
    253         * "sampleToken":"A30_B","material":{"class":"dbnp.data.Term","id":6,"accession":"BTO:0000131", 
    254         * "name":"blood plasma","ontology":{"class":"Ontology","id":2}},"Remarks":null, 
    255         * "Text on vial":"T70.91709057820039","Sample measured volume":null} 
    256         */ 
     275         * REST resource for data modules. 
     276         * Username and password should be supplied via HTTP Basic Authentication. 
     277         * One specific sample of a given Assay. 
     278         * 
     279         * @param       assayToken      String (id of some Assay in GSCF) 
     280         * @param       consumer        consumer name of the calling module 
     281         * @param       token           token for the authenticated user (e.g. session_id) 
     282         * @return As a JSON object list, for each sample in that assay: 
     283         * @return 'name' (Sample name, which is unique) 
     284         * @return 'material' (Sample material) 
     285         * @return 'subject' (The name of the subject from which the sample was taken) 
     286         * @return 'event' (the name of the template of the SamplingEvent describing the sampling) 
     287         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string) 
     288         * 
     289         * Example REST call (without authentication): 
     290     * http://localhost:8080/gscf/rest/getSample/sam?assayToken=PPS3_SAM&sampleToken=A30_B 
     291     * 
     292         * Returns the JSON object: 
     293         * {"subject":"A30","event":"Liver extraction","startTime":"1 week, 1 hour", 
     294         * "sampleToken":"A30_B","material":{"class":"dbnp.data.Term","id":6,"accession":"BTO:0000131", 
     295         * "name":"blood plasma","ontology":{"class":"Ontology","id":2}},"Remarks":null, 
     296         * "Text on vial":"T70.91709057820039","Sample measured volume":null} 
     297         */ 
    257298        def getSample = { 
    258299                def items = [:] 
     
    279320        } 
    280321 
     322        /** 
     323         * Returns the authorization level the user has for a given study. 
     324         * 
     325         * If no studyToken is given, a 400 (Bad Request) error is given. 
     326         * If the given study doesn't exist, a 404 (Not found) error is given. 
     327         * 
     328         * @param       consumer        consumer name of the calling module 
     329         * @param       token           token for the authenticated user (e.g. session_id) 
     330         * @return      JSON Object 
     331         * @return  { isOwner: true/false, 'canRead': true/false, 'canWrite': true/false } 
     332         */ 
    281333        def getAuthorizationLevel = { 
    282                 // in future the users authorization level will be based on authorization model          
    283334                if( params.studyToken ) { 
    284335                        def id = params.studyToken 
    285336                        def study = Study.find( "from Study as s where s.code=?", [id]) 
    286                         if(study) study.subjects.each { subjects.push it.name } 
     337 
     338                        if( !study ) { 
     339                                response.sendError(404) 
     340                                return false 
     341                        } 
     342 
     343                        def user = AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token ); 
     344                        render( 'isOwner': study.isOwner(user), 'canRead': study.canRead(user), 'canWrite': study.canWrite(user) ) 
     345                } else { 
     346                        response.sendError(400) 
     347                        return false 
    287348                } 
    288  
    289                 def perm = study.getPermissions(requestUser) 
    290                  
    291                 render ('isOwner': study.isOwner(requestUser), 
    292                         'create': perm.create, 'read':perm.read, 
    293                         'update': perm.update, 'delete':perm.delete 
    294                         ) as JSON 
    295349    } 
    296350} 
  • trunk/grails-app/services/dbnp/authentication/AuthenticationService.groovy

    r976 r983  
    1919class AuthenticationService { 
    2020    def SpringSecurityService 
     21    static final int expiryTime = 60; // Number of minutes a remotely logged in user remains active 
    2122 
    2223    boolean transactional = true 
    2324 
    24     protected boolean isLoggedIn() { 
    25       def principal = SpringSecurityService.getPrincipal() 
    26  
    27       // If the user is logged in, the principal should be a GrailsUser object. 
    28       // If the user is not logged in, the principal is the 'anonymous username' 
    29       // i.e. a string 
    30       if( principal instanceof GrailsUser ) { 
    31           return true; 
    32       } 
    33  
    34       return false; 
     25    public boolean isLoggedIn() { 
     26        return SpringSecurityService.isLoggedIn(); 
    3527    } 
    3628 
    37     protected SecUser getLoggedInUser() { 
     29    public SecUser getLoggedInUser() { 
    3830      def principal = SpringSecurityService.getPrincipal() 
    3931 
     
    4739      return null; 
    4840    } 
     41 
     42    /** 
     43     * Logs a user in for a remote session 
     44     */ 
     45    public boolean logInRemotely( String consumer, String token, SecUser user ) { 
     46        // Make sure there is no other logged in user anymore 
     47        logOffRemotely( consumer, token ) 
     48 
     49        def SAUser = new SessionAuthenticatedUser( consumer: consumer, token: token, secUser: user, expiryDate: createExpiryDate() ) 
     50 
     51        return SAUser.save(flush: true) 
     52    } 
     53     
     54    public boolean logOffRemotely( String consumer, String token ) { 
     55        def user = getSessionAuthenticatedUser(consumer, token) 
     56         
     57        if( user ) 
     58            user.delete() 
     59         
     60        return true 
     61    } 
     62 
     63    /** 
     64     * Checks whether a user is logged in from a remote consumer with the 
     65     * given token 
     66     */ 
     67    public boolean isRemotelyLoggedIn( String consumer, String token ) { 
     68        // Remove expired users, otherwise they will be kept in the database forever 
     69        removeExpiredTokens() 
     70 
     71        // Check whether a user exists 
     72        def user = getSessionAuthenticatedUser(consumer, token) 
     73 
     74        // Check whether the user is logged in. Since we don't want to return a 
     75        // user, we explicitly return true or false 
     76        if( user ) { 
     77                        // The expiry date should be reset 
     78                        updateExpiryDate( user ) 
     79 
     80            return true 
     81                } else { 
     82            return false 
     83                } 
     84    } 
     85 
     86    /** 
     87     * Returns the user that is logged in remotely 
     88     */ 
     89    public SecUser getRemotelyLoggedInUser( String consumer, String token ) { 
     90        // Remove expired users, otherwise they will be kept in the database forever 
     91        removeExpiredTokens() 
     92 
     93        // Check whether a user exists 
     94        def user = getSessionAuthenticatedUser(consumer, token) 
     95 
     96        return user ? user.secUser : null 
     97    } 
     98 
     99    /** 
     100     * Removes all tokens for remote logins that have expired 
     101     */ 
     102    protected boolean removeExpiredTokens() { 
     103        SessionAuthenticatedUser.executeUpdate("delete SessionAuthenticatedUser u where u.expiryDate < :expiryDate", [ expiryDate: new Date() ]) 
     104    } 
     105 
     106    /** 
     107         * Returns the currently logged in user from the database or null if no user is logged in 
     108         */ 
     109        protected SessionAuthenticatedUser getSessionAuthenticatedUser( String consumer, String token ) { 
     110        def c = SessionAuthenticatedUser.createCriteria() 
     111        def result = c.get { 
     112                and { 
     113                        eq( "consumer", consumer) 
     114                        eq( "token", token) 
     115                        gt( "expiryDate", new Date()) 
     116                } 
     117        } 
     118 
     119        if( result ) 
     120            return result 
     121        else 
     122            return null 
     123    } 
     124 
     125        /** 
     126         * Returns the expiry date for a user that is active now. 
     127         */ 
     128        protected Date createExpiryDate() { 
     129                // Compute expiryDate 
     130                long now = new Date().getTime(); 
     131                return new Date( now + AuthenticationService.expiryTime * 60 * 1000 ); 
     132 
     133        } 
     134 
     135        /** 
     136         * Resets the expiry date of the given user. This should be called every time 
     137         * an action occurs with this user. That way, if (in case of a timeout of 60 minutes) 
     138         * he logs in and returns 50 minutes later, he will keep a timeout value of 
     139         * 60 minutes, instead of only 10 minutes. 
     140         */ 
     141        protected boolean updateExpiryDate( SessionAuthenticatedUser user ) { 
     142                user.expiryDate = createExpiryDate() 
     143                return user.save( flush: true ) 
     144        } 
    49145} 
  • trunk/grails-app/views/login/auth.gsp

    r976 r983  
    6767                                        <input type='submit' value='Login' /> 
    6868                                </p> 
     69 
     70                                <g:if test="${redirectUrl}"> 
     71                                  <g:hiddenField name="spring-security-redirect" value="${redirectUrl}" /> 
     72                                </g:if> 
    6973                        </form> 
    7074                </div>